Avatar ·

Замена #ifdef в Swift: условная компиляция и конфигурации сборки

Всем привет! Я только начал изучать Swift после C++ и столкнулся с проблемой. В C++ я часто использовал директивы препроцессора `#ifdef`, `#ifndef`, `#endif` для условной компиляции кода — например, чтобы включать отладочный код только в Debug-сборке или поддерживать разные платформы. В Swift я не нашел прямого аналога `#ifdef`. Пытался написать что-то вроде: ```swift #ifdef DEBUG print("Отладочная информация") #endif ``` Но Xcode выдает ошибку. Почитал документацию — там есть `#if`, но он работает не так, как я привык. Например, я хочу: 1. Добавлять дополнительное логирование только в Debug-режиме. 2. Исключать целые блоки кода для iOS, но оставлять для macOS. 3. Скрывать тестовые заглушки (stubs) в релизной сборке. Как правильно организовать условную компиляцию в Swift? Есть ли способ определить свои флаги, как `#define MY_FLAG`? Буду благодарен за примеры с объяснением. <<>>

Условная компиляция в 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-подход, в отличие от разбросанных директив препроцессора.