Как Swift отличает симулятор от реального устройства: полный разбор

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как Swift отличает симулятор от реального устройства? Полный разбор под капотом

Каждый iOS-разработчик хотя бы раз сталкивался с ситуацией, когда код отлично работает в симуляторе, но падает на реальном устройстве. Или наоборот — функция, которая использует камеру или акселерометр, недоступна в симуляторе. В такие моменты жизненно важно уметь отличать, где запущено приложение: на физическом iPhone/iPad или в виртуальной среде Xcode. Эта тема — настоящий хит на Stack Overflow (тысячи просмотров!), и сегодня мы разберем её до самого дна. Вы узнаете не только готовые решения, но и поймёте, как они работают на уровне компилятора и препроцессора.

Почему это вообще нужно?

Симулятор — мощный инструмент, но он не эмулирует всё железо. Например, вы не получите доступ к:

  • Камере (только эмуляция через веб-камеру Mac)
  • Акселерометру и гироскопу
  • Настоящему звонковому модулю
  • Face ID (только в симуляторе с T2/M1)
  • Чипу Secure Enclave

Кроме того, архитектуры процессоров разные: на реальном устройстве — ARM64, а на симуляторе (на Mac с Intel) — x86_64. Даже на Apple Silicon симулятор работает через Rosetta 2, что вносит свои нюансы. Именно для таких случаев нам и нужна проверка на симулятор в Swift.

Классический способ: TARGET_OS_SIMULATOR

Самый популярный и рекомендуемый Apple метод — использовать директиву препроцессора TARGET_OS_SIMULATOR. Она определена в заголовочном файле TargetConditionals.h, который автоматически подключается к каждому проекту Swift через модуль Darwin.

Вот как выглядит стандартный код:

#if targetEnvironment(simulator)
    print("Запущено в симуляторе")
    // Здесь можно эмулировать поведение камеры или GPS
#else
    print("Запущено на реальном устройстве")
    // Настоящая камера, настоящий GPS
#endif

Просто, элегантно, работает. Но давайте копнём глубже — что на самом деле происходит, когда компилятор видит эту конструкцию?

Под капотом: как работает targetEnvironment

В Swift конструкция #if targetEnvironment(simulator) — это не магия, а результат работы препроцессора. Когда Xcode собирает проект для симулятора, он добавляет специальный флаг компилятора -DTARGET_OS_SIMULATOR=1. Соответственно, для реального устройства этот флаг равен 0 или отсутствует.

Вот цепочка преобразований:

  1. Xcode определяет, куда собирается билд (схема сборки)
  2. Если выбран симулятор — в OTHER_SWIFT_FLAGS добавляется -D TARGET_OS_SIMULATOR
  3. Компилятор Swift видит эту константу и обрабатывает блок #if
  4. Весь код внутри блока #else (для реального устройства) вырезается из бинарника

Это значит, что такой код не просто выполняется во время рантайма — он компилируется по-разному для разных платформ. Вы получаете два разных бинарных файла, и в каждом из них нет лишнего кода. Экономия места и скорости!

Альтернативный метод: проверка архитектуры

До появления targetEnvironment разработчики использовали хитрый трюк с архитектурными флагами. Он до сих пор работает и иногда полезен для обратной совместимости.

#if arch(x86_64) || arch(i386)
    print("Симулятор (Intel Mac)")
#elseif arch(arm64)
    print("Реальное устройство или симулятор на Apple Silicon")
#endif

Но здесь есть подвох! На Mac с Apple Silicon (M1, M2, M3) симулятор тоже работает на ARM64. Поэтому просто проверить arch(arm64) недостаточно — это будет истина и для реального iPhone, и для симулятора на M1. Именно поэтому Apple и ввела targetEnvironment(simulator) — он заточен именно под эту задачу.

Тонкости работы на Apple Silicon

С 2020 года, когда вышли Mac на ARM, ситуация изменилась. Раньше симулятор всегда был x86_64, а устройство — arm64. Теперь и то, и другое может быть arm64. Как быть? Используйте комбинацию:

#if arch(arm64) && targetEnvironment(simulator)
    print("Симулятор на Apple Silicon")
#elseif arch(arm64)
    print("Реальное устройство (или симулятор на Intel через Rosetta?)")
#endif

На практике лучше доверять targetEnvironment — он учитывает все эти нюансы автоматически.

Рантайм-проверка: когда #if не подходит

Бывают ситуации, когда вам нужно проверить симулятор не на этапе компиляции, а во время выполнения. Например, вы загружаете динамическую библиотеку или используете reflection. Для этого подходит ProcessInfo:

import Foundation

func isRunningOnSimulator() -> Bool {
    #if targetEnvironment(simulator)
        return true
    #else
        return false
    #endif
}

// Использование
if isRunningOnSimulator() {
    // Симулируем ответ от камеры
} else {
    // Открываем настоящую камеру
}

Обратите внимание: сама функция компилируется по-разному для разных сред, но вызывается она как обычный метод. Это гибридный подход — проверка всё ещё на этапе компиляции, но результат доступен в рантайме.

Полный список флагов для разных сценариев

В Swift доступны и другие условные флаги компиляции. Вот полная таблица для iOS-разработчика:

ФлагОписание
targetEnvironment(simulator)Истина только в симуляторе (рекомендуется)
arch(arm64)ARM-архитектура (устройства и M1/M2/M3)
arch(x86_64)Intel-архитектура (симулятор на Intel Mac)
os(iOS)Любая iOS-среда (устройство или симулятор)
os(iOS) && !targetEnvironment(simulator)Только реальное устройство

Комбинируя их, вы можете достичь любой точности проверки. Например, чтобы отличить iPad от iPhone в симуляторе:

#if targetEnvironment(simulator) && os(iOS)
    if UIDevice.current.userInterfaceIdiom == .pad {
        print("iPad симулятор")
    }
#endif

Частые ошибки и как их избежать

Даже опытные разработчики иногда попадаются на этих граблях:

Блоги

Рекомендаций по книгам