Avatar ·

Как получить высоту верхнего и нижнего отступа Safe Area (safeAreaInsets) в iOS?

Приветствую, коллеги! Я разрабатываю приложение под iOS на Swift и столкнулся с проблемой: мне нужно программно получить точные значения верхнего и нижнего отступов Safe Area (например, чтобы учесть высоту статус-бара и "чёлки" на iPhone X и новее, а также нижнюю панель Home Indicator). Я пробовал использовать `view.safeAreaInsets.top` и `view.safeAreaInsets.bottom` в методе `viewDidLoad()`, но эти значения возвращают 0. Я читал, что это происходит потому, что Safe Area еще не сформирована на момент загрузки view. В качестве обходного пути я пробовал вызывать этот код в `viewDidLayoutSubviews()`, но там получаю странные значения — например, `top` равен 44, а `bottom` — 34, хотя на iPhone 14 Pro Max должно быть 59 и 34 соответственно (с учетом динамического острова). Вот мой текущий код: ```swift override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let topInset = view.safeAreaInsets.top let bottomInset = view.safeAreaInsets.bottom print("Top: \(topInset), Bottom: \(bottomInset)") } ``` На симуляторе iPhone 14 Pro Max я получаю `Top: 44.0, Bottom: 34.0`, хотя ожидаю `Top: 59.0`. Почему так? И как правильно получить актуальные значения Safe Area, которые будут работать на всех устройствах, включая те, что без "чёлки" (например, iPhone SE)? Также интересует, как получить эти значения до того, как view будет полностью отрисован, чтобы использовать их для расчета layout в коде. Заранее спасибо за помощь! <<>>

Правильное получение 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(). Используйте его только для одноразовых расчетов.