5.14. Рекомендации по разработке на Swift
Рекомендации по разработке на Swift
Введение в культуру кода Swift
Swift сочетает в себе современные подходы к безопасности типов, производительности и читаемости. Рекомендации в этой главе формируют основу для создания предсказуемого, поддерживаемого кода в экосистеме Apple. Соблюдение единых правил упрощает совместную работу, снижает количество ошибок и ускоряет внесение изменений в проект.
Культура кода начинается с понимания философии языка: безопасность по умолчанию, минимизация неопределённости, предпочтение значимых именований над комментариями. Эти принципы проявляются во всех аспектах разработки — от именования переменных до архитектурных решений.
Требования по именованию
Общие правила именования
Имена в коде передают намерения разработчика. Качественное именование заменяет необходимость в поясняющих комментариях и ускоряет понимание логики.
| Элемент языка | Нотация | Пример |
|---|---|---|
| Класс, структура, перечисление | UpperCamelCase | NetworkService, UserProfile |
| Протокол | UpperCamelCase с суффиксом able, ible, ing | Configurable, Downloadable, Loading |
| Функция, метод | lowerCamelCase | fetchUserData(), calculateTotal() |
| Переменная, константа | lowerCamelCase | currentUser, maxRetryCount |
| Параметр функции | lowerCamelCase | completionHandler, searchQuery |
| Глобальная константа | lowerCamelCase | appVersion, apiBaseURL |
Имена должны быть конкретными и отражать назначение элемента. Избегайте сокращений, если они не являются общепринятыми в предметной области (URL, ID, JSON). Предпочитайте полные слова: identifier вместо id, configuration вместо config, если контекст не делает сокращение очевидным.
Именование булевых значений
Булевы свойства и переменные начинаются с глаголов-помощников is, has, can, should:
var isEnabled: Bool
var hasPermission: Bool
var canRetry: Bool
var shouldRefresh: Bool
Избегайте отрицательных формулировок в именах: isNotValid заменяется на isValid с инвертированной логикой использования.
Именование коллекций
Коллекции именуются во множественном числе, отражая содержимое:
let users: [User]
let activeTasks: [Task]
let pendingRequests: Set<URLRequest>
Исключение составляют случаи, когда коллекция представляет собой единое целое с собственной семантикой: coordinatePair, rgbColor.
Именование параметров функций
Swift поддерживает внешние и внутренние имена параметров. Внешнее имя улучшает читаемость вызова функции, внутреннее — удобство реализации:
func addTask(_ task: Task, to queue: TaskQueue) { ... }
// Вызов читается как предложение
addTask(newTask, to: backgroundQueue)
Первый параметр часто не имеет внешнего имени (обозначается _), если его назначение очевидно из имени функции. Последующие параметры получают осмысленные внешние имена.
Требования по оформлению кода
Отступы и пробелы
Используйте четыре пробела для отступов. Табуляция не применяется. Пробелы размещаются:
- После ключевых слов
if,guard,while,for,switch - Вокруг бинарных операторов (
+,-,==,&&) - После запятых в списках параметров и элементов коллекций
- Перед открывающей скобкой в вызовах функций
// Правильно
if user.isActive && user.hasPermission {
process(user: currentUser)
}
// Неправильно
if(user.isActive&&user.hasPermission){
process( user:currentUser )
}
Фигурные скобки
Открывающая фигурная скобка размещается в той же строке, что и объявление:
func fetchData() {
// тело функции
}
class DataManager {
// тело класса
}
Закрывающая скобка находится на отдельной строке без дополнительных отступов. Пустые строки разделяют логические блоки внутри функций и методов.
Переносы строк в выражениях
Длинные выражения разбиваются по операторам. Оператор размещается в начале новой строки:
let total = basePrice
+ shippingCost
- discountAmount
+ tax
let userQuery = users
.filter { $0.isActive }
.sorted { $0.lastLoginDate > $1.lastLoginDate }
.prefix(10)
Вызовы функций с множеством параметров оформляются вертикально:
networkService.request(
endpoint: .userProfile(id: userId),
method: .get,
parameters: nil,
headers: authHeaders,
completion: handleResponse
)
Пустые строки
Одна пустая строка разделяет:
- Методы и свойства внутри типа
- Логические блоки внутри функции
- Завершающую скобку функции и следующее объявление
Две пустые строки разделяют типы (классы, структуры, перечисления) на уровне файла.
Структура проекта и организация файлов
Группировка по функциональности
Проект организуется по функциональным модулям, а не по типам артефактов. Каждая функциональная область содержит все необходимые компоненты внутри себя:
Sources/
├── Core/
│ ├── Networking/
│ │ ├── API.swift
│ │ ├── RequestBuilder.swift
│ │ └── ResponseParser.swift
│ ├── Persistence/
│ │ ├── Database.swift
│ │ └── Cache.swift
│ └── Utilities/
│ ├── DateExtensions.swift
│ └── StringExtensions.swift
├── Features/
│ ├── Authentication/
│ │ ├── Models/
│ │ │ └── User.swift
│ │ ├── Views/
│ │ │ └── LoginView.swift
│ │ ├── ViewModels/
│ │ │ └── LoginViewModel.swift
│ │ └── Services/
│ │ └── AuthService.swift
│ └── Dashboard/
│ ├── Models/
│ ├── Views/
│ ├── ViewModels/
│ └── Services/
└── Resources/
├── Assets.xcassets/
└── Localizable.strings
Такая структура упрощает навигацию: все файлы, относящиеся к одной функции, находятся рядом. При масштабировании проекта новые разработчики быстрее ориентируются в кодовой базе.
Именование файлов
Имя файла совпадает с основным типом, объявленным внутри. Расширения именуются по шаблону ОсновнойТип+Дополнение.swift:
User.swift
User+Equatable.swift
User+Codable.swift
Date+Extensions.swift
String+Trimming.swift
Это позволяет быстро находить расширения через поиск по имени файла и понимать их назначение без открытия содержимого.
Проектирование типов
Выбор между классом и структурой
Структуры применяются по умолчанию. Классы используются в случаях:
- Необходимости наследования
- Работы с фреймворками Apple, требующими классов (например,
UIViewController) - Когда объект должен иметь идентичность и передаваться по ссылке
Структуры обеспечивают безопасность за счёт копирования значений и отсутствия общего состояния между экземплярами. Это упрощает многопоточную работу и снижает вероятность ошибок.
Протоколы вместо наследования
Протоколы создают гибкие абстракции без жёсткой иерархии классов. Предпочитайте композицию через протоколы наследованию:
protocol DataProvider {
func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}
class NetworkProvider: DataProvider { ... }
class CacheProvider: DataProvider { ... }
class CompositeProvider: DataProvider {
private let network: NetworkProvider
private let cache: CacheProvider
// делегирует запросы обоим провайдерам
}
Такой подход упрощает тестирование (лёгкая подмена реализаций) и позволяет комбинировать поведение без глубоких иерархий наследования.
Перечисления с ассоциированными значениями
Перечисления в Swift мощнее аналогов в других языках благодаря ассоциированным значениям. Используйте их для моделирования состояний и вариантов данных:
enum NetworkResult {
case success(Data)
case failure(Error)
case cancelled
}
enum AuthenticationState {
case unauthenticated
case authenticating
case authenticated(User)
case locked(attempts: Int)
}
Такие перечисления делают обработку состояний явной и исключают недостижимые комбинации данных.
Расширения для организации кода
Расширения группируют функциональность по смыслу внутри одного типа:
// MARK: - Lifecycle
extension ViewController {
override func viewDidLoad() { ... }
override func viewWillAppear(_ animated: Bool) { ... }
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { ... }
}
// MARK: - Private Helpers
extension ViewController {
private func configureUI() { ... }
private func loadData() { ... }
}
Комментарии // MARK: - создают визуальные разделители в навигаторе Xcode и упрощают поиск методов по категориям.
Работа с функциями и методами
Ограничение количества параметров
Функции принимают не более пяти параметров. При необходимости передачи большего количества данных создаётся структура-параметр:
struct SearchParameters {
let query: String
let category: Category?
let minPrice: Double?
let maxPrice: Double?
let sortBy: SortOption
let page: Int
}
func searchProducts(using parameters: SearchParameters) { ... }
Такой подход улучшает читаемость вызовов и упрощает добавление новых параметров без изменения сигнатуры функции.
Предпочтение чистых функций
Чистые функции не имеют побочных эффектов и возвращают результат, зависящий только от входных параметров. Они проще тестируются и комбинируются:
// Чистая функция
func calculateTax(for amount: Double, rate: Double) -> Double {
return amount * rate
}
// Функция с побочным эффектом
func saveUser(_ user: User) {
database.insert(user)
analytics.track(event: .userSaved)
}
Стремитесь выносить побочные эффекты (работа с сетью, диском, интерфейсом) в отдельные компоненты, оставляя бизнес-логику в чистых функциях.
Использование замыканий для асинхронных операций
Асинхронные операции возвращают результат через замыкания с типом Result:
func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {
network.request(endpoint: .user(id)) { result in
completion(result.map(User.init))
}
}
// Использование
fetchUser(id: "123") { result in
switch result {
case .success(let user):
self.display(user)
case .failure(let error):
self.showError(error)
}
}
Тип Result делает обработку успеха и ошибки явной и исключает возможность игнорирования ошибок.
Обработка ошибок
Использование типов ошибок
Создавайте конкретные типы ошибок вместо использования общих NSError или строковых сообщений:
enum NetworkError: Error {
case invalidURL
case timeout
case httpStatus(code: Int)
case decoding(Error)
case cancelled
}
Конкретные типы ошибок позволяют точно обрабатывать разные ситуации и избегать сравнения строк или кодов вручную.
Ранний выход через guard
Проверки условий оформляются через guard с ранним выходом. Это уменьшает вложенность кода и делает основную логику более заметной:
func process(user: User?) {
guard let user = user else { return }
guard user.isActive else { return }
guard user.hasValidSubscription else { return }
// Основная логика обработки пользователя
updateUserStats(for: user)
sendNotification(to: user)
}
Каждая проверка завершается выходом из функции, что гарантирует валидность состояния для последующего кода.
Логирование ошибок
Ошибки логируются в точке их обработки, а не в точке возникновения. Это предотвращает дублирование записей и позволяет контролировать уровень детализации:
func loadData() {
dataService.fetch { result in
switch result {
case .success(let data):
self.updateUI(with: data)
case .failure(let error):
Logger.error("Failed to load data: \(error.localizedDescription)")
self.showErrorMessage()
}
}
}
Логирование включает контекст операции и информацию, достаточную для диагностики без раскрытия конфиденциальных данных.
Комментарии и документация
Документация через DocC
Swift поддерживает встроенную систему документации на основе комментариев. Используйте синтаксис для генерации документации:
/// Загружает профиль пользователя по идентификатору
///
/// Запрос выполняется к эндпоинту `/users/{id}` с авторизацией через Bearer токен.
/// При ошибке сети повторяет запрос до трёх раз с экспоненциальной задержкой.
///
/// - Parameters:
/// - id: Уникальный идентификатор пользователя. Должен соответствовать формату UUID.
/// - completion: Замыкание, вызываемое по завершении запроса. Содержит результат операции.
///
/// - Throws: `NetworkError.invalidURL` если идентификатор имеет неверный формат.
///
/// - Note: Результат кэшируется на 5 минут для повторных запросов с тем же идентификатором.
func fetchUserProfile(id: String, completion: @escaping (Result<UserProfile, Error>) -> Void) {
// реализация
}
Комментарии размещаются непосредственно перед объявлением элемента. Они описывают назначение, параметры, возвращаемые значения и особенности поведения.
Комментарии в коде
Комментарии поясняют нетривиальные решения или временные ограничения:
// Используем синхронный вызов из-за ограничений фреймворка,
// который не поддерживает асинхронные замыкания в этой версии.
let result = legacyFramework.process(data)
// Временное решение до внедрения нового алгоритма сортировки в версии 2.0
users.sort { $0.lastName < $1.lastName }
Избегайте комментариев, повторяющих код. Вместо комментария улучшайте именование или структуру:
// Плохо: комментарий повторяет код
// Увеличиваем счётчик на единицу
counter += 1
// Хорошо: код самодокументирован
pageViewCount += 1
Закомментированный код
Закомментированный код удаляется из репозитория. История изменений хранится в системе контроля версий. Наличие закомментированного кода создаёт неопределённость: актуален ли он, можно ли удалить, зачем он оставлен.
Архитектурные подходы
Разделение ответственности
Каждый компонент отвечает за одну область функциональности:
- Модели содержат данные и бизнес-логику предметной области
- Сервисы реализуют взаимодействие с внешними системами (сеть, база данных)
- Вью-модели преобразуют данные для отображения и обрабатывают действия пользователя
- Представления отвечают за визуальное отображение и пользовательский ввод
Такое разделение упрощает тестирование и замену компонентов без влияния на остальную систему.
Внедрение зависимостей
Зависимости передаются в компонент извне, а не создаются внутри:
class UserProfileViewModel {
private let userService: UserServiceProtocol
private let imageLoader: ImageLoaderProtocol
init(userService: UserServiceProtocol, imageLoader: ImageLoaderProtocol) {
self.userService = userService
self.imageLoader = imageLoader
}
}
Внедрение зависимостей позволяет легко подменять реализации в тестах и избегает жёсткой привязки к конкретным классам.
Реактивные подходы
Для управления состоянием и потоками данных применяются реактивные фреймворки (Combine, RxSwift) или асинхронные последовательности (AsyncSequence):
// С помощью Combine
cancellable = userService.currentUser
.map { $0.displayName }
.assign(to: \.text, on: nameLabel)
// С помощью async/await
Task {
for await displayName in userService.currentUserDisplayName() {
nameLabel.text = displayName
}
}
Реактивные подходы упрощают обработку асинхронных событий и автоматическое обновление интерфейса при изменении данных.
Тестирование
Структура тестов
Тесты организуются параллельно основному коду с сохранением иерархии:
Tests/
├── UnitTests/
│ ├── Core/
│ │ └── Networking/
│ │ └── RequestBuilderTests.swift
│ └── Features/
│ └── Authentication/
│ └── LoginViewModelTests.swift
└── UITests/
└── AuthenticationFlowTests.swift
Каждый тестовый файл содержит один тестовый класс, соответствующий тестируемому компоненту.
Наименование тестов
Имена тестовых методов описывают проверяемое поведение:
func testLoginViewModel_WhenCredentialsValid_CallsCompletionWithSuccess() { ... }
func testLoginViewModel_WhenNetworkFails_ReturnsError() { ... }
func testUserProfile_WhenCreatedWithValidData_HasCorrectDisplayName() { ... }
Такой формат делает результаты тестов самодокументированными при падении.
Моки и заглушки
Для изоляции тестируемого компонента зависимости заменяются моками:
class MockUserService: UserServiceProtocol {
var fetchUserCallCount = 0
var fetchUserResult: Result<User, Error> = .success(testUser)
func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {
fetchUserCallCount += 1
completion(fetchUserResult)
}
}
Моки фиксируют вызовы методов и возвращают предопределённые значения, что позволяет проверять взаимодействие компонентов.
Работа с памятью и производительность
Избегание сильных циклических ссылок
Замыкания захватывают self слабо при работе с объектами:
networkService.fetchData { [weak self] result in
guard let self = self else { return }
self.handle(result)
}
Использование [weak self] предотвращает утечки памяти в асинхронных операциях. Для значимых типов (структур) захват не требуется — они копируются.
Отложенная инициализация
Ресурсоёмкие объекты инициализируются по требованию:
private lazy var imageCache: NSCache<NSString, UIImage> = {
let cache = NSCache<NSString, UIImage>()
cache.totalCostLimit = 100 * 1024 * 1024 // 100 MB
return cache
}()
Ленивые свойства инициализируются один раз при первом обращении и сохраняют значение для последующих использований.
Оптимизация коллекций
При работе с большими коллекциями применяются эффективные операции:
// Предпочтительно
users.filter { $0.isActive }.map { $0.id }
// Вместо цепочки с промежуточными массивами
let activeUsers = users.filter { $0.isActive }
let ids = activeUsers.map { $0.id }
Метод reduce(into:) предпочтительнее reduce(_:_:) для мутабельных накопителей, так как избегает копирования на каждой итерации.
Современные практики Swift
Использование свойств-обёрток
Свойства-обёртки упрощают повторяющуюся логику доступа к данным:
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
struct AppSettings {
@UserDefault(key: "isDarkMode", defaultValue: false)
static var isDarkMode: Bool
}
Обёртки инкапсулируют шаблонный код и делают его переиспользуемым.
Асинхронность с async/await
Современный код использует нативную поддержку асинхронности вместо замыканий:
func loadUserProfile() async throws -> UserProfile {
let userData = try await networkService.fetch(endpoint: .profile)
let avatarData = try await imageService.load(url: userData.avatarURL)
return UserProfile(userData: userData, avatar: avatarData)
}
// Использование
Task {
do {
let profile = try await loadUserProfile()
display(profile)
} catch {
showError(error)
}
}
Код с async/await читается последовательно, избегает "ада колбэков" и упрощает обработку ошибок через try/catch.
Актёры для многопоточности
Актёры изолируют состояние и предотвращают гонки данных:
actor MessageQueue {
private var messages: [Message] = []
func add(_ message: Message) {
messages.append(message)
}
func next() -> Message? {
messages.isEmpty ? nil : messages.removeFirst()
}
}
Доступ к состоянию актёра происходит последовательно, что гарантирует безопасность без явной синхронизации.