Проблема с локализацией
Стандартный подход к локализации в iOS — строки вида NSLocalizedString("welcome_title", comment: ""). Это работает, но у него есть минусы: нет автодополнения, легко опечататься в ключе, рефакторинг болезненный.
Популярное решение — SwiftGen, который генерирует типобезопасный код из Localizable.strings. Но это внешний инструмент, его нужно встраивать в CI, обновлять, объяснять новым разработчикам.
Swift Macros дают возможность сделать то же самое нативно, без внешних зависимостей.
Что хотим получить
// Хотим писать вот так:
@Localizable
enum Strings {
static let welcomeTitle = "Добро пожаловать"
static let loginButton = "Войти"
static let errorMessage = "Что-то пошло не так"
}
// А использовать так (с автодополнением):
label.text = Strings.welcomeTitle
button.setTitle(Strings.loginButton, for: .normal)
Создаём макрос
Создайте новый Swift Package: File → New → Package → Swift Macro. Xcode создаст структуру с двумя таргетами — сам макрос и его реализация.
// Package.swift
.target(
name: "LocalizableMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]
)
Реализация
Макрос типа @attached(member) позволяет добавлять членов к типу. Нам нужно для каждого static let заменить строку на вызов NSLocalizedString:
public struct LocalizableMacro: MemberAttributeMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntax,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
guard let varDecl = member.as(VariableDeclSyntax.self),
varDecl.bindingSpecifier.tokenKind == .keyword(.let) else {
return []
}
return ["@LocalizedString"]
}
}
Важно: Реализация макроса компилируется как отдельный плагин и запускается на хосте (вашем Mac), не на целевой платформе. Это значит что в ней нельзя использовать Foundation или UIKit.
Тестирование макроса
Swift предоставляет специальный фреймворк для тестирования макросов:
import MacroTesting
import XCTest
final class LocalizableTests: XCTestCase {
func testExpansion() {
assertMacroExpansion(
"""
@Localizable
enum Strings {
static let title = "Hello"
}
""",
expandedSource: """
enum Strings {
static let title = NSLocalizedString("title", comment: "")
}
""",
macros: ["Localizable": LocalizableMacro.self]
)
}
}
Итог
Полная реализация с обработкой edge cases занимает около 150 строк. Код доступен по запросу. Макрос уже используем в двух проектах на проде, проблем не было.
Swift Macros — это то направление куда движется язык. Рекомендую начать изучать сейчас, пока экосистема только формируется.