Когда код работает на симуляторе, но ломается на реальном устройстве: разбираемся с UI_USER_INTERFACE_IDIOM() в Swift
Вы когда-нибудь писали приложение, которое идеально выглядит на симуляторе iPhone 14 Pro, а на iPad Mini превращается в кашу? Или наоборот — ваш модный iPad-интерфейс съезжает на старом iPod touch? Знакомая боль, правда?
Разработчики iOS часто сталкиваются с необходимостью узнать, на каком устройстве запущено приложение. Казалось бы, что может быть проще? Но дьявол, как всегда, кроется в деталях. Легендарный вопрос на Stack Overflow про UI_USER_INTERFACE_IDIOM() собрал тысячи просмотров, а это значит, что даже опытные программисты иногда путаются в нюансах.
В этой статье мы разложим по полочкам всё, что нужно знать об определении устройства в Swift. Никакой воды — только практика, живые примеры и ответы на самые частые вопросы.
Что такое UI_USER_INTERFACE_IDIOM() и почему о нём говорят?
Это старая добрая функция из эпохи Objective-C. Она возвращает значение типа UIUserInterfaceIdiom, которое указывает, с каким типом устройства мы имеем дело: iPhone, iPad, TV или CarPlay.
Вот как это выглядело в классическом Objective-C:
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
// Это iPad
} else {
// Это iPhone или iPod touch
}
Но Swift — язык более строгий и безопасный. Прямое использование этой C-функции не всегда удобно. На смену пришёл более современный подход через UIDevice.current.
Как правильно определить устройство в Swift: два рабочих способа
Способ №1 — классика через UIDevice (рекомендуется):
import UIKit
let deviceIdiom = UIDevice.current.userInterfaceIdiom
switch deviceIdiom {
case .phone:
print("Это iPhone или iPod touch")
case .pad:
print("Это iPad")
case .tv:
print("Это Apple TV")
case .carPlay:
print("Это CarPlay")
case .mac:
print("Это Mac Catalyst или Mac")
@unknown default:
print("Неизвестное устройство (возможно, будущее)")
}
Важно: Кейс .mac появился в iOS 14+. Если ваше приложение поддерживает более старые версии, используйте @unknown default для обработки будущих типов.
Способ №2 — совместимость с Objective-C кодом (если нужно):
if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.phone {
print("Работаем на телефоне")
}
Оба варианта корректны, но первый — более идиоматичный для Swift.
Частые вопросы и подводные камни
1. Почему на iPad mini определяется iPhone?
Это самый популярный вопрос. Вы запускаете приложение на iPad mini, а userInterfaceIdiom упорно возвращает .phone. В чём дело?
Ответ простой и неочевидный: режим масштабирования (Scale Mode). Если в настройках вашего приложения (Target → General → Deployment Info) выбран режим "iPhone", то даже на физическом iPad система будет эмулировать iPhone.
Решение: проверьте настройки проекта.
- Зайдите в Target вашего приложения
- General → Deployment Info
- Devices: выберите "Universal" (если приложение должно работать на всех устройствах)
Если вы выбрали "iPhone Only", то на iPad приложение будет работать в режиме совместимости (как на большом iPhone), и idiom будет .phone.
2. Как отличить iPhone от iPod touch?
UI_USER_INTERFACE_IDIOM() не различает iPhone и iPod touch — оба возвращают .phone. Для точного определения модели нужно использовать UIDevice.current.model или modelName через sysctl.
Пример расширения для получения точного имени модели:
import UIKit
extension UIDevice {
var modelName: String {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.compactMap { $0.value as? Int8 }
.filter { $0 != 0 }
.map { String(UnicodeScalar(UInt8($0))) }
.joined()
return identifier
}
}
// Использование:
print(UIDevice.current.modelName) // Например: "iPhone14,2" или "iPod9,1"
Затем вы можете сопоставить идентификатор с известными моделями через словарь или парсить строку.
3. А что с iPadOS 13+ и поддержкой мыши/трекпада?
С появлением поддержки мыши и трекпада на iPad, многие разработчики пытаются использовать userInterfaceIdiom для определения, подключено ли указывающее устройство. Так делать нельзя! Idiom отвечает только за тип устройства, а не за подключённые аксессуары.
Для проверки подключения мыши или трекпада используйте:
if UIDevice.current.userInterfaceIdiom == .pad {
// Проверяем, есть ли у нас указательное устройство
if UIPointerInteraction.isSupported {
print("Трекпад или мышь подключены (или эмулируются)")
}
}
Но лучше проверять через GCController.controllers() или слушать уведомления GCMouseDidConnect.
4. Почему на симуляторе всё работает, а на реальном устройстве — нет?
Симулятор — штука хитрая. Он может эмулировать разные устройства, но не всегда точно передаёт их поведение. Например, на симуляторе iPad с iPhone-режимом масштабирования userInterfaceIdiom вернёт .phone, и вы можете не заметить подвоха.
Совет: Всегда тестируйте определение устройства на реальных девайсах. Хотя бы на одном iPad и одном iPhone.
Продвинутые техники: когда простого idiom недостаточно
Адаптивный интерфейс через size classes
В современном iOS-разработке редко нужно жёстко привязываться к типу устройства. Гораздо гибче использовать Size Classes (горизонтальный и вертикальный классы размера).
Пример в коде:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.horizontalSizeClass == .regular {
// Широкий экран (iPad в альбомной, iPhone Plus в альбомной)
showSidebar()
} else {
// Узкий экран (iPhone в портрете)
hideSidebar()
}
}
Этот подход позволяет адаптировать интерфейс под любые устройства, включая будущие модели с нестандартными размерами.
Проверка на наличие Face ID или Touch ID
Иногда нужно определить не тип устройства, а его биометрические возможности:
import