Перейти к основному содержимому

5.14. Рекомендации по разработке на Swift

Разработчику Архитектору

Рекомендации по разработке на Swift

Введение в культуру кода Swift

Swift сочетает в себе современные подходы к безопасности типов, производительности и читаемости. Рекомендации в этой главе формируют основу для создания предсказуемого, поддерживаемого кода в экосистеме Apple. Соблюдение единых правил упрощает совместную работу, снижает количество ошибок и ускоряет внесение изменений в проект.

Культура кода начинается с понимания философии языка: безопасность по умолчанию, минимизация неопределённости, предпочтение значимых именований над комментариями. Эти принципы проявляются во всех аспектах разработки — от именования переменных до архитектурных решений.

Требования по именованию

Общие правила именования

Имена в коде передают намерения разработчика. Качественное именование заменяет необходимость в поясняющих комментариях и ускоряет понимание логики.

Элемент языкаНотацияПример
Класс, структура, перечислениеUpperCamelCaseNetworkService, UserProfile
ПротоколUpperCamelCase с суффиксом able, ible, ingConfigurable, Downloadable, Loading
Функция, методlowerCamelCasefetchUserData(), calculateTotal()
Переменная, константаlowerCamelCasecurrentUser, maxRetryCount
Параметр функцииlowerCamelCasecompletionHandler, searchQuery
Глобальная константаlowerCamelCaseappVersion, 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()
}
}

Доступ к состоянию актёра происходит последовательно, что гарантирует безопасность без явной синхронизации.