Avatar ·

Как определить, собирается ли приложение для симулятора или реального устройства в Swift

Всем привет! Я новичок в iOS-разработке и столкнулся с такой проблемой. В моём приложении есть функционал, связанный с камерой и доступом к контактам. На реальном устройстве всё работает отлично, но когда я запускаю проект в симуляторе Xcode (например, iPhone 15 Pro), приложение падает с ошибкой, потому что симулятор не поддерживает камеру. Я хочу добавить проверку в коде, чтобы на симуляторе просто выводить заглушку, а на реальном устройстве открывать камеру. Я пробовал использовать `#if TARGET_OS_SIMULATOR`, но компилятор ругается, что это неопределённый идентификатор. Потом нашёл `#if targetEnvironment(simulator)`, но не уверен, правильно ли я его применяю. Мой текущий код выглядит так: ```swift import UIKit class CameraViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() #if targetEnvironment(simulator) print("Запущено на симуляторе — камера недоступна") showSimulatorPlaceholder() #else print("Запущено на устройстве — открываем камеру") openCamera() #endif } func showSimulatorPlaceholder() { // Показываем заглушку } func openCamera() { // Код для работы с камерой } } ``` Но я не уверен, что это лучший способ. Подскажите, как правильно и надёжно определить среду выполнения? Может быть, есть другие варианты? Заранее спасибо! <<>>

Правильный способ: директива targetEnvironment(simulator)

Вы уже на верном пути! Действительно, в Swift для определения симулятора используется директива условной компиляции #if targetEnvironment(simulator). Она появилась в Swift 4.1 и является единственным официально рекомендуемым Apple способом для этой задачи. Ваш код почти правильный, но давайте разберём его детально и покажем лучшие практики.

Почему это работает?

Директива targetEnvironment(simulator) проверяется на этапе компиляции. Если сборка предназначена для симулятора, компилятор включает блок кода внутри #if. В противном случае (для реального устройства) выполняется блок #else. Это чисто компиляторная проверка — она не влияет на производительность во время выполнения.

Полный пример с пояснениями

import UIKit
import AVFoundation

class CameraViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Проверка на симулятор на этапе компиляции
        #if targetEnvironment(simulator)
            print("⚠️ Приложение запущено на симуляторе")
            showSimulatorPlaceholder()
        #else
            // Код для реального устройства
            print("✅ Приложение запущено на устройстве")
            checkCameraPermissionAndOpen()
        #endif
    }
    
    private func showSimulatorPlaceholder() {
        let label = UILabel()
        label.text = "Камера недоступна в симуляторе"
        label.textAlignment = .center
        label.textColor = .gray
        label.frame = view.bounds
        view.addSubview(label)
    }
    
    private func checkCameraPermissionAndOpen() {
        // Только для реального устройства
        let status = AVCaptureDevice.authorizationStatus(for: .video)
        switch status {
        case .authorized:
            openCamera()
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { granted in
                DispatchQueue.main.async {
                    if granted {
                        self.openCamera()
                    } else {
                        self.showPermissionDeniedAlert()
                    }
                }
            }
        case .denied, .restricted:
            showPermissionDeniedAlert()
        @unknown default:
            fatalError("Неизвестный статус авторизации")
        }
    }
    
    private func openCamera() {
        // Реализация открытия камеры
        print("📷 Камера открыта")
    }
    
    private func showPermissionDeniedAlert() {
        let alert = UIAlertController(
            title: "Нет доступа к камере",
            message: "Разрешите доступ в Настройках",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

Важные моменты и лучшие практики

  • Не используйте #if TARGET_OS_SIMULATOR: Это устаревшая константа из Objective-C, которая недоступна в Swift напрямую. Она может привести к ошибкам компиляции.
  • Не путайте с runtime-проверками: Не пытайтесь использовать UIDevice.current.model или ProcessInfo.processInfo.environment для определения симулятора — это ненадёжно и может не сработать в будущих версиях Xcode.
  • Создайте удобный computed property: Для многократного использования определите свойство:
extension UIDevice {
    static var isSimulator: Bool {
        #if targetEnvironment(simulator)
            return true
        #else
            return false
        #endif
    }
}

// Использование:
if UIDevice.isSimulator {
    print("Запущено на симуляторе")
} else {
    print("Запущено на устройстве")
}

Когда это действительно нужно?

  • Работа с аппаратными функциями (камера, микрофон, датчики, Face ID).
  • Тестирование push-уведомлений (симулятор их не поддерживает).
  • Отладка специфичных для устройства функций (например, работа с Core Bluetooth).
  • Автоматическое переключение между мок-данными и реальными данными.

Заключение

Ваш подход с #if targetEnvironment(simulator) абсолютно верен. Единственное, что я бы посоветовал — вынести проверку в extension или отдельную функцию, чтобы код был чище и переиспользуемым. И помните: эта директива работает только на этапе компиляции, поэтому код для симулятора не попадёт в билд для App Store.

<<>>

Альтернативный подход: использование ProcessInfo для runtime-проверки

Хотя основное решение — это директива компиляции, иногда требуется проверка во время выполнения (runtime). Например, если вы загружаете стороннюю библиотеку, которая ведёт себя по-разному на симуляторе и устройстве. В таких случаях можно использовать ProcessInfo.processInfo.environment:

import Foundation

struct EnvironmentChecker {
    static var isSimulator: Bool {
        return ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] != nil
    }
    
    static var simulatorName: String? {
        return ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"]
    }
}

// Использование:
if EnvironmentChecker.isSimulator {
    print("Запущено на симуляторе: \(EnvironmentChecker.simulatorName ?? "неизвестно")")
} else {
    print("Запущено на реальном устройстве")
}

Когда это полезно?

  • Если вы подключаете фреймворки, которые не компилируются для симулятора (например, некоторые обёртки над Metal или CoreNFC).
  • Для логирования или отладки, когда нужно узнать точное название симулятора.
  • При работе с динамическими библиотеками, которые загружаются во время выполнения.

Недостатки runtime-подхода

  • Код для устройства всё равно будет загружен в память, что может вызвать краш при обращении к неподдерживаемым API.
  • Переменная окружения SIMULATOR_DEVICE_NAME может измениться в будущих версиях Xcode.

Поэтому для большинства сценариев предпочтительнее использовать #if targetEnvironment(simulator), а runtime-проверку применять только как дополнение в особых случаях.