Замена #ifdef в Swift: условная компиляция и конфигурации сборки
Условная компиляция в Swift: #if и флаги компиляции
В Swift нет прямого аналога #ifdef, но есть более мощный механизм — директива #if, которая работает на этапе компиляции. Она использует предопределенные и пользовательские флаги компиляции (compiler flags).
Базовый синтаксис
#if DEBUG
print("Отладочная информация")
#else
print("Релизная сборка")
#endif
Как добавить свои флаги
В Xcode:
- Выберите цель (Target) → Build Settings → Swift Compiler — Custom Flags
- В секции Active Compilation Conditions добавьте свои флаги (например,
MY_FLAG) для нужной конфигурации (Debug/Release)
В командной строке:
swift build -Xswiftc "-D" -Xswiftc "MY_FLAG"
Пример: отладка и платформы
#if DEBUG
let apiEndpoint = "http://localhost:8080/api"
LogManager.shared.logLevel = .verbose
#else
let apiEndpoint = "https://production.example.com/api"
LogManager.shared.logLevel = .error
#endif
// Условный код для разных платформ
#if os(iOS)
import UIKit
typealias Color = UIColor
#elseif os(macOS)
import AppKit
typealias Color = NSColor
#endif
Проверка версий Swift и SDK
#if swift(>=5.5)
// Используем async/await
await someAsyncFunction()
#else
// Используем completion handlers
someAsyncFunction { result in ... }
#endif
#if canImport(Combine)
import Combine
// Используем Combine
#else
// Fallback на делегаты
#endif
Практический пример: тестовые заглушки
class DataService {
func fetchData() -> [String] {
#if DEBUG && !RELEASE_TESTING
// В отладке возвращаем заглушки
return ["Mock1", "Mock2"]
#elseif RELEASE_TESTING
// Специальный режим для тестирования
return TestDataGenerator.generate()
#else
// Реальный запрос к серверу
return NetworkManager.shared.fetch()
#endif
}
}
Для этого нужно добавить флаг RELEASE_TESTING в конфигурацию Release (если нужно) или создать отдельную конфигурацию сборки.
Важные ограничения
- Нельзя использовать
#defineдля констант — используйтеletилиenum - Флаги компиляции — это просто метки, они не могут иметь значений (как
#define VALUE 42) - Для сложной конфигурации используйте Xcode Configuration Files (.xcconfig)
Альтернативный подход: Configuration Files
Создайте файл Config.xcconfig:
// Для Debug
MY_API_KEY = debug_key_123
MY_FLAGS = DEBUG
// Для Release
MY_API_KEY = prod_key_456
MY_FLAGS = RELEASE
В коде используйте Bundle.main.infoDictionary или кастомный plist для чтения конфигурации во время выполнения.
Заключение
Используйте #if + флаги компиляции для статической условной компиляции. Для динамической конфигурации (например, разные URL) используйте plist или environment variables. Это чище и безопаснее, чем препроцессор в C++.
Альтернативный подход: Протоколы и инъекция зависимостей
Вместо разбрасывания #if DEBUG по всему коду, лучше использовать архитектурный подход с протоколами и фабриками. Это делает код чище и тестируемее.
protocol Logger {
func log(_ message: String)
}
class DebugLogger: Logger {
func log(_ message: String) {
print("[DEBUG] \(message)")
}
}
class ReleaseLogger: Logger {
func log(_ message: String) {
// В релизе ничего не делаем или отправляем в Crashlytics
}
}
// Фабрика с условной компиляцией
class LoggerFactory {
static func create() -> Logger {
#if DEBUG
return DebugLogger()
#else
return ReleaseLogger()
#endif
}
}
// Использование
let logger = LoggerFactory.create()
logger.log("Важное сообщение")
Преимущества:
- Единственное место с
#if— фабрика - Легко тестировать, подменяя реализации
- Код следует принципам SOLID
- Можно легко расширять (добавить TestLogger)
Для платформозависимого кода аналогично — используйте протоколы с разными реализациями под каждую платформу. Это современный Swift-подход, в отличие от разбросанных директив препроцессора.