Что такое макросы в Swift и зачем они вообще нужны
Если вы пишете на Swift хотя бы пару лет, то наверняка замечали, сколько однотипного кода приходится писать снова и снова. Макросы — это ответ Apple на эту проблему. Они появились в Swift 5.9 и с тех пор серьёзно эволюционировали, особенно в Swift 6.2.
Суть простая: макросы генерируют код за вас на этапе компиляции. Но в отличие от макросов в C/Objective-C (которые, честно говоря, были источником головной боли), макросы Swift типобезопасны — компилятор проверяет и вход, и выход.
В 2026 году знание макросов стало практически обязательным для Middle- и Senior-разработчиков на iOS. Компании вроде Avito, Ozon и Т-Банк спрашивают про них на собеседованиях. И это логично — вся экосистема Apple, от SwiftData до SwiftUI и Swift Testing, построена на макросах. Так что давайте разберёмся, как они работают.
Два вида макросов: автономные и прикреплённые
Swift делит макросы на две категории: автономные (freestanding) и прикреплённые (attached). Это ключевое разделение, и его стоит запомнить.
Автономные макросы (#)
Автономные макросы начинаются со знака # и живут сами по себе — не привязаны к какому-то объявлению. Сгенерированный код вставляется прямо на место вызова.
// Автономный макрос-выражение
let url = #URL("https://api.example.com/users")
// Автономный макрос-объявление
#warning("Эта функция ещё не реализована")
Прикреплённые макросы (@)
Прикреплённые макросы начинаются с @ и всегда применяются к существующему объявлению — классу, структуре, свойству или функции. Они расширяют это объявление дополнительным кодом.
// Прикреплённый макрос @Observable
@Observable
final class UserViewModel {
var name: String = ""
var email: String = ""
}
// Прикреплённый макрос @Model из SwiftData
@Model
class Task {
var title: String
var isCompleted: Bool
}
Все роли макросов: полный справочник
Каждый макрос выполняет определённую роль — она определяет, какой именно код будет сгенерирован. Один макрос может совмещать несколько ролей (и это, кстати, используется очень часто). Вот полный перечень ролей в Swift 6.2.
Роли автономных макросов
Expression — макрос-выражение
Создаёт фрагмент кода, возвращающий значение. Самый распространённый тип — вы будете встречать его повсюду.
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) =
#externalMacro(module: "MyMacrosPlugin", type: "StringifyMacro")
// Использование:
let result = #stringify(2 + 3) // (5, "2 + 3")
Declaration — макрос-объявление
Создаёт одно или несколько новых объявлений: структуры, функции, переменные.
@freestanding(declaration, names: arbitrary)
macro makeArrayND(n: Int)
// Использование:
#makeArrayND(n: 3) // Генерирует тип Array3D
Роли прикреплённых макросов
Peer — макрос-соседей
Создаёт новые объявления рядом с тем, к которому применён. Типичный кейс — автогенерация completion-handler-версий async-функций.
@attached(peer, names: overloaded)
public macro AddCompletionHandler(
parameterName: String = "completionHandler"
)
// Применение:
@AddCompletionHandler
func fetchUser(id: Int) async throws -> User { ... }
// Макрос генерирует рядом:
// func fetchUser(id: Int, completionHandler: @escaping (Result<User, Error>) -> Void) { ... }
Accessor — макрос-аксессоров
Добавляет геттеры и сеттеры (get, set, willSet, didSet) к свойству. В отличие от property wrappers (которые работают в рантайме), этот макрос отрабатывает на этапе компиляции.
@attached(accessor)
macro UserDefault<T>(key: String, defaultValue: T)
// Применение:
@UserDefault(key: "username", defaultValue: "")
var username: String
// Макрос генерирует get/set, обращающиеся к UserDefaults
MemberAttribute — макрос атрибутов членов
Добавляет атрибуты ко всем членам типа. Звучит абстрактно, но на практике это очень удобно:
@attached(memberAttribute)
macro AllPublished()
// Применение:
@AllPublished
class SettingsStore: ObservableObject {
var theme: Theme = .light // Автоматически получает @Published
var fontSize: Int = 14 // Автоматически получает @Published
}
Member — макрос-членов
Добавляет новые объявления внутрь типа: инициализаторы, вычисляемые свойства, методы.
@attached(member, names: named(init(dictionary:)), named(dictionary))
macro DictionaryStorage()
// Применение:
@DictionaryStorage
struct UserProfile {
var name: String
var age: Int
// Макрос генерирует init(dictionary:) и свойство dictionary
}
Extension — макрос расширений
Генерирует полноценные extension, включая соответствие протоколам. Этот макрос заменил более ранний Conformance-макрос.
@attached(extension, conformances: Equatable, Hashable)
public macro AutoEquatable()
// Применение:
@AutoEquatable
struct Coordinate {
var x: Double
var y: Double
}
// Макрос генерирует extension Coordinate: Equatable, Hashable { ... }
Body — макрос тела функции
Генерирует тело функции, у которой его нет. Отлично подходит для автоматического логирования и трассировки — я лично использовал его именно для этого.
@attached(body)
macro Traced()
// Применение:
@Traced
func processPayment(amount: Double) -> Bool
// Макрос генерирует тело с автоматическим логированием
Создание собственного макроса: пошаговое руководство
Хватит теории — давайте напишем настоящий макрос. Создадим #URL, который проверяет строку URL на валидность прямо на этапе компиляции. Если URL кривой — проект просто не соберётся.
Шаг 1: Создание пакета Swift Macro
Откройте Xcode и выберите File → New → Package, затем шаблон Swift Macro. Назовите пакет, например, SafeURLMacro.
Xcode создаст пакет с такой структурой:
SafeURLMacro/
├── Package.swift
├── Sources/
│ ├── SafeURLMacro/ # Объявление макроса (публичный API)
│ ├── SafeURLMacroMacros/ # Реализация макроса (compiler plugin)
│ └── SafeURLMacroClient/ # Клиент для тестирования (main.swift)
└── Tests/
└── SafeURLMacroTests/ # Юнит-тесты
Шаг 2: Объявление макроса
В файле Sources/SafeURLMacro/SafeURLMacro.swift объявляем сигнатуру:
import Foundation
/// Макрос, проверяющий валидность URL на этапе компиляции.
/// Если строка не является корректным URL, компиляция завершится ошибкой.
@freestanding(expression)
public macro URL(_ stringLiteral: String) -> URL =
#externalMacro(module: "SafeURLMacroMacros", type: "URLMacro")
Директива #externalMacro — это мост между объявлением и типом, который реализует логику макроса в отдельном модуле плагина.
Шаг 3: Реализация логики макроса
А вот тут начинается самое интересное. В файле Sources/SafeURLMacroMacros/URLMacro.swift пишем саму логику:
import SwiftSyntax
import SwiftSyntaxMacros
import Foundation
public struct URLMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
// 1. Извлекаем первый аргумент макроса
guard let argument = node.arguments.first?.expression,
let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
segments.count == 1,
case .stringSegment(let literalSegment)? = segments.first
else {
throw MacroError.message(
"#URL требует один статический строковый литерал"
)
}
// 2. Проверяем валидность URL на этапе компиляции
guard let _ = Foundation.URL(string: literalSegment.content.text) else {
throw MacroError.message(
"Недопустимый URL: \"\(literalSegment.content.text)\""
)
}
// 3. Генерируем безопасный код
return "URL(string: \(argument))!"
}
}
enum MacroError: Error, CustomStringConvertible {
case message(String)
var description: String {
switch self {
case .message(let text): return text
}
}
}
Обратите внимание на метод expansion — он получает синтаксическое дерево (AST) через SwiftSyntax, анализирует его и возвращает новый узел ExprSyntax. Именно этот узел и подставляется в итоговый код.
Шаг 4: Регистрация плагина
Не забудьте зарегистрировать макрос в файле плагина:
import SwiftCompilerPlugin
import SwiftSyntaxMacros
@main
struct SafeURLMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
URLMacro.self,
]
}
Шаг 5: Тестирование макроса
Одно из главных преимуществ макросов — их реально легко тестировать. SwiftSyntax даёт функцию assertMacroExpansion, которая сравнивает сгенерированный код с ожидаемым:
import SwiftSyntaxMacrosTestSupport
import XCTest
@testable import SafeURLMacroMacros
final class URLMacroTests: XCTestCase {
let testMacros: [String: Macro.Type] = [
"URL": URLMacro.self,
]
func testValidURL() throws {
assertMacroExpansion(
"""
#URL("https://api.example.com")
""",
expandedSource: """
URL(string: "https://api.example.com")!
""",
macros: testMacros
)
}
func testInvalidURL() throws {
assertMacroExpansion(
"""
#URL("это не URL")
""",
expandedSource: """
#URL("это не URL")
""",
diagnostics: [
DiagnosticSpec(
message: "Недопустимый URL: \"это не URL\"",
line: 1,
column: 1
)
],
macros: testMacros
)
}
}
Шаг 6: Использование в проекте
Готово! Теперь макрос можно подключить в любом проекте как Swift Package:
import SafeURLMacro
let apiURL = #URL("https://api.example.com/v2/users")
// Компилятор проверит валидность URL прямо на этапе сборки
Встроенные макросы экосистемы Apple
Apple сама активно использует макросы в своих фреймворках. Понимание того, как работают встроенные макросы, здорово помогает эффективнее пользоваться платформой.
@Observable — управление состоянием в SwiftUI
Макрос @Observable — это, пожалуй, самый используемый макрос в SwiftUI-проектах. Он совмещает роли Member, MemberAttribute и Extension, автоматически добавляя отслеживание изменений ко всем свойствам.
@Observable
final class CounterViewModel {
var count = 0 // Автоматически отслеживается
var title = "Счётчик" // Автоматически отслеживается
}
// В отличие от ObservableObject + @Published,
// SwiftUI обновляет View только при изменении
// конкретного свойства, которое использует View
Кстати, чтобы увидеть, какой код генерирует макрос, кликните правой кнопкой на @Observable в Xcode → Expand Macro. Рекомендую сделать это хотя бы раз — очень наглядно показывает, сколько кода макрос пишет за вас.
@Model — модели данных в SwiftData
Макрос @Model генерирует весь код для работы с хранилищем: соответствие PersistentModel, конфигурацию схемы и отслеживание изменений.
@Model
class Article {
var title: String
var content: String
var createdAt: Date
@Relationship(deleteRule: .cascade)
var tags: [Tag]
init(title: String, content: String) {
self.title = title
self.content = content
self.createdAt = .now
}
}
#Preview — предпросмотр интерфейса
Макрос #Preview заменил громоздкий PreviewProvider и работает как со SwiftUI, так и с UIKit:
// SwiftUI
#Preview("Экран профиля") {
ProfileView(user: .mock)
}
// UIKit (доступно с Xcode 15)
#Preview("Ячейка таблицы") {
let cell = UserTableViewCell()
cell.configure(with: .mock)
return cell
}
@Test — тестирование в Swift Testing
Макрос @Test из Swift Testing поддерживает параметризованные тесты и метаданные. Если вы ещё не пробовали Swift Testing — самое время начать:
import Testing
@Test("Сложение положительных чисел", .tags(.math))
func addition() {
#expect(2 + 2 == 4)
}
@Test("Парсинг email", arguments: [
"[email protected]",
"[email protected]"
])
func validEmail(email: String) {
#expect(email.contains("@"))
}
Новое в Swift 6.2: Observations и производительность
Swift 6.2 принёс важные улучшения для макросов и Observation framework. Давайте посмотрим, что изменилось.
Тип Observations — AsyncSequence для наблюдения за изменениями
Новый тип Observations (SE-0475) создаёт AsyncSequence, который эмитирует значения при изменении свойств @Observable-объекта. Раньше наблюдать за изменениями можно было только из SwiftUI, а теперь — из любого места: сервисов, менеджеров, бизнес-логики.
@Observable
final class Thermometer {
var temperature: Double = 20.0
var unit: String = "Celsius"
}
let sensor = Thermometer()
let readings = Observations { (sensor.temperature, sensor.unit) }
Task {
for await (temp, unit) in readings {
print("Температура: \(temp)° \(unit)")
}
}
// Изменения доставляются транзакционно:
// несколько синхронных изменений объединяются в одно обновление
sensor.temperature = 25.0
sensor.unit = "Fahrenheit"
// → Одно обновление: (25.0, "Fahrenheit")
Быстрая компиляция макросов
Swift 6.2 наконец решает одну из главных болей — время компиляции. SwiftPM теперь поддерживает предкомпилированные зависимости swift-syntax, полностью убирая долгий шаг сборки из исходников. Раньше компиляция swift-syntax занимала 15–20 секунд в Debug и больше 2 минут в Release — теперь это в прошлом.
Отладка макросов в Xcode
Xcode предоставляет удобные инструменты для работы с макросами. Вот что стоит знать.
Просмотр развёрнутого кода
Чтобы увидеть, что именно генерирует макрос:
- Кликните правой кнопкой мыши на макросе в редакторе
- Выберите Expand Macro
- Или через Shift + Command + A → Expand Macro
Работает для любых макросов — и встроенных, и ваших собственных.
Точки останова в сгенерированном коде
После раскрытия макроса через Expand Macro можно ставить брейкпоинты на любой строке сгенерированного кода. Отладка работает как обычно — никакой магии.
Обработка ошибок
Если макрос выбрасывает ошибку, компилятор покажет её как стандартную ошибку компиляции. Кроме того, через метод addDiagnostic можно генерировать предупреждения, ошибки и даже Fix-It-подсказки:
// Пример кастомной ошибки в реализации макроса
context.addDiagnostic(
Diagnostic(
node: node,
message: MyDiagnosticMessage.notAStruct,
fixIt: FixIt(
message: MyFixItMessage.addStructKeyword,
changes: [/* изменения кода */]
)
)
)
Лучшие практики для продакшена
Макросы — штука мощная, но требуют аккуратного подхода. Вот что я бы порекомендовал на основе реального опыта.
1. Внедряйте макросы точечно
Не нужно переписывать весь проект ради макросов. Начните с мест, где есть очевидный повторяющийся код: модели данных, сетевой слой, конфигурация DI-контейнеров.
2. Начните со встроенных макросов
Прежде чем писать свои макросы, освойте @Observable, #Preview, @Model и @Test. Регулярно используйте Expand Macro — это лучший способ понять, что происходит под капотом.
3. Пишите тесты для каждого макроса
Серьёзно, не пропускайте этот шаг. Функция assertMacroExpansion делает тестирование макросов детерминированным — вы сравниваете сгенерированный код с ожидаемым. Идеально для TDD.
4. Следите за временем компиляции
Зависимость от swift-syntax утяжеляет первую сборку. В Swift 6.2 ситуация значительно лучше благодаря предкомпилированным зависимостям, но на CI всё равно стоит кешировать артефакты.
5. Обеспечьте детерминированность
Золотое правило: одинаковый вход → одинаковый выход. Если макрос детерминирован, инкрементальная сборка может пропускать пересборку файлов, где ничего не изменилось.
6. Документируйте, что генерирует макрос
Разработчики, которые будут использовать ваш макрос, должны понимать, что именно он создаёт. Добавляйте примеры развёрнутого кода прямо в документацию.
Практический пример: макрос @Builder
Давайте создадим ещё один макрос — на этот раз прикреплённый. Он будет автоматически генерировать builder-интерфейс для структур. Это реальный паттерн, который встречается в продакшен-проектах постоянно.
Объявление макроса
@attached(member, names: arbitrary)
@attached(extension, conformances: Buildable)
public macro Builder() = #externalMacro(
module: "BuilderMacroPlugin",
type: "BuilderMacro"
)
Обратите внимание: макрос совмещает две роли — member (для методов-сеттеров внутри типа) и extension (для соответствия протоколу Buildable).
Использование
@Builder
struct NetworkRequest {
var url: URL
var method: String = "GET"
var headers: [String: String] = [:]
var timeout: TimeInterval = 30
}
// Сгенерированный builder-интерфейс:
let request = NetworkRequest(url: apiURL)
.withMethod("POST")
.withHeaders(["Authorization": "Bearer token"])
.withTimeout(60)
Без макроса пришлось бы вручную писать каждый метод withProperty. С макросом — они генерируются автоматически на основе AST структуры. Экономия времени колоссальная.
Популярные макросы от сообщества
Open-source-экосистема макросов растёт быстро. Вот что стоит знать для продакшен-проектов:
- @Mockable — генерация моков для протоколов. Добавляете макрос — получаете мок-класс. Незаменим для юнит-тестов.
- @CodableKey — настройка маппинга свойств при JSON-кодировании без ручного написания
CodingKeys. - @EnumAllCases — расширенная генерация всех кейсов для enum с associated values (стандартный
CaseIterableтут не работает). - TCA Macros — макросы из Composable Architecture от Point-Free, которые убирают boilerplate для состояний и действий.
Полный каталог макросов можно найти в репозитории Swift-Macros на GitHub — там сообщество поддерживает актуальный курированный список.
Часто задаваемые вопросы
Чем макросы Swift отличаются от макросов в C/Objective-C?
В C макросы — это простая текстовая замена на этапе препроцессора, без проверки типов и с кучей подводных камней. Макросы Swift работают с абстрактным синтаксическим деревом (AST), проверяются компилятором и запускаются в изолированном процессе (sandbox). Плюс они аддитивны — могут только добавлять код, но не удалять или менять существующий.
Увеличивают ли макросы время компиляции?
Первая сборка действительно может быть медленнее из-за swift-syntax: раньше это было 15–20 секунд в Debug и больше 2 минут в Release. Но в Swift 6.2 благодаря предкомпилированным зависимостям проблема практически решена. Инкрементальные сборки тоже оптимизированы — детерминированные макросы не пересобираются.
Можно ли использовать макросы с UIKit?
Да, макросы — это возможность языка Swift, а не конкретного фреймворка. Они работают везде: UIKit, SwiftUI, серверный Swift (Vapor), консольные приложения. Тот же #Preview поддерживает и SwiftUI Views, и UIKit UIViewController начиная с Xcode 15.
Нужно ли знать SwiftSyntax?
Для использования готовых макросов — нет. Но для создания своих — да, без SwiftSyntax не обойтись. Вся логика генерации строится на работе с узлами синтаксического дерева. Начните с шаблонного макроса stringify, который Xcode создаёт автоматически при генерации нового пакета Swift Macro.
Когда стоит создавать свой макрос?
Если у вас есть паттерн, который повторяется в трёх и более местах и при этом достаточно однообразен для автоматизации — макрос имеет смысл. Если код уникален или используется в одном-двух местах — лучше обойтись протоколами и дженериками. Помните: три одинаковые строки кода лучше преждевременной абстракции.