Как получить высоту верхнего и нижнего отступа Safe Area (safeAreaInsets) в iOS?
Правильное получение safeAreaInsets в iOS
Проблема, с которой вы столкнулись, типична для начинающих разработчиков. Основная причина — неверное время получения отступов и некорректное понимание того, что такое safeAreaInsets на разных устройствах. Давайте разберемся по порядку.
Почему safeAreaInsets.top = 44, а не 59?
Значение 44 — это стандартная высота статус-бара на iPhone с "чёлочкой" (начиная с iPhone X) в портретной ориентации. Вы ожидали 59, потому что на iPhone 14 Pro Max высота динамического острова (Dynamic Island) действительно 59 pt, но safeAreaInsets.top возвращает высоту статус-бара, а не всей "чёлки". Динамический остров — это визуальный элемент, который накладывается поверх контента, но Safe Area по-прежнему считается от нижней границы статус-бара. Таким образом, 44 pt — абсолютно корректное значение для всех iPhone с Face ID (кроме iPhone 14 Pro/Pro Max, где тоже 44, так как статус-бар физически выше, но Safe Area это не учитывает).
На iPhone SE (с Touch ID) safeAreaInsets.top будет равен 20 pt (высота статус-бара), а на iPhone 14 Pro Max с динамическим островом — 44 pt. Это не ошибка, так задумано Apple: Safe Area гарантирует, что ваш контент не перекрывается системными элементами, но не обязательно следует за их визуальной границей.
Правильный способ получения safeAreaInsets
Вы правы, что viewDidLoad() не подходит — Safe Area еще не установлена. viewDidLayoutSubviews() — это правильное место, но есть нюанс: этот метод может вызываться несколько раз (например, при повороте устройства). Лучше использовать viewSafeAreaInsetsDidChange() — специальный метод, который вызывается каждый раз, когда меняются отступы Safe Area.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
let topInset = view.safeAreaInsets.top
let bottomInset = view.safeAreaInsets.bottom
let leftInset = view.safeAreaInsets.left
let rightInset = view.safeAreaInsets.right
print("Top: \(topInset), Bottom: \(bottomInset), Left: \(leftInset), Right: \(rightInset)")
// Используйте эти значения для обновления layout
updateLayout(with: topInset, bottom: bottomInset)
}
private func updateLayout(with topInset: CGFloat, bottom bottomInset: CGFloat) {
// Пример: установка отступов для scrollView
scrollView.contentInset = UIEdgeInsets(top: topInset, left: 0, bottom: bottomInset, right: 0)
scrollView.scrollIndicatorInsets = scrollView.contentInset
}
}
Как получить safeAreaInsets до отрисовки view?
Если вам нужно получить отступы до того, как view будет полностью отрисован (например, для расчета высоты в viewDidLoad()), используйте additionalSafeAreaInsets или обратитесь к UIApplication.shared.keyWindow?.safeAreaInsets. Однако keyWindow может быть nil на ранних этапах, поэтому лучше использовать UIWindowScene (iOS 13+):
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) {
let topInset = keyWindow.safeAreaInsets.top
let bottomInset = keyWindow.safeAreaInsets.bottom
print("Ранние отступы: Top: \(topInset), Bottom: \(bottomInset)")
}
Универсальный подход для всех устройств
Чтобы ваше решение работало на всех устройствах (включая старые iPhone SE и iPad), всегда проверяйте, что safeAreaInsets не равны нулю, и используйте их в viewSafeAreaInsetsDidChange(). Если вы используете Auto Layout, просто привязывайте элементы к safeAreaLayoutGuide — это избавит от необходимости вручную вычислять отступы:
let myView = UIView()
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
NSLayoutConstraint.activate([
myView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
myView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
myView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
myView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])
Этот подход гарантирует, что ваш контент всегда будет в безопасной зоне, независимо от устройства и ориентации.
<<Альтернативный подход: использование UIEdgeInsets и кастомный расчет
Если вам нужно получить именно высоту "челки" или динамического острова для анимаций (например, чтобы подложить фон под статус-бар), то safeAreaInsets не подойдет — он возвращает только безопасную зону. Вместо этого можно использовать UIApplication.shared.statusBarFrame.height (устарело в iOS 13) или получать размер статус-бара через keyWindow.windowScene?.statusBarManager?.statusBarFrame:
if let statusBarManager = view.window?.windowScene?.statusBarManager {
let statusBarHeight = statusBarManager.statusBarFrame.height
print("Высота статус-бара: \(statusBarHeight)")
}
Для нижнего отступа (Home Indicator) можно использовать фиксированное значение 34 pt для устройств без кнопки Home, но лучше довериться safeAreaInsets.bottom. Если вам нужно получить отступы до загрузки view (например, в init контроллера), создайте вспомогательный метод, который будет вызываться после первого layoutSubviews:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard safeAreaTop == 0 else { return } // Выполнить только один раз
safeAreaTop = view.safeAreaInsets.top
safeAreaBottom = view.safeAreaInsets.bottom
// Ваш код инициализации
}
Этот способ проще, но менее гибкий, чем viewSafeAreaInsetsDidChange(). Используйте его только для одноразовых расчетов.
Ответов пока нет.
Войдите чтобы оставить ответ