При реализации пользовательских представлений контроллера представления, где применить представленные ограничения представления?

Я прочитал всю документацию Apple о пользовательских модальных презентациях и не смог найти этот ответ. При представлении контроллера представления с использованием пользовательской анимации мы можем вернуть 3 вещи (если переход не является интерактивным; интерактивный может вернуть 4): контроллер представления и два контроллера анимации (один для настоящего и один для отклонения).

И в контроллере представления, и в настоящем контроллере анимации ни один из кодов Apple не содержит ограничений. Степень обсуждения фреймов - рекомендация Apple установить фрейм представленного представления контроллера представления в существующем контроллере анимации (ниже):

// Always add the "to" view to the container.
// And it doesn't hurt to set its start frame.
[containerView addSubview:toView];
toView.frame = toViewStartFrame;

... и что это.

У меня проблема в том, что строка состояния двойной высоты не распознается этими представленными контроллерами представления в симуляторе (и устройствами, которыми я владею). Точнее, ни одно решение не работает во всех симуляторах - решения, которые работают в новых симуляторах (например, iPhone 8), не работают в старых симуляторах (например, iPhone 5). Если я позволю Apple обработать презентацию с использованием анимации UIKit по умолчанию, строка состояния двойной высоты будет хорошо обрабатываться представленным контроллером представления; поэтому я могу предположить, что ограничения в представленном контроллере представления не являются проблемой.

Поэтому я без особой удачи обратился к методам контроллера презентации containerViewDidLayoutSubviews и containerViewWillLayoutSubviews:

override func containerViewDidLayoutSubviews() {
    super.containerViewDidLayoutSubviews()
    presentedViewController.view.frame = containerView!.bounds
}

Приведенный выше код работает в симуляторе только при первом появлении строки состояния двойной высоты; с этого момента этот метод перестает отвечать на запросы. Чтобы проверить это:

override func containerViewDidLayoutSubviews() {
    super.containerViewDidLayoutSubviews()
    presentedViewController.view.frame = containerView!.bounds
    print("did layout")
}

Приведенный выше метод останавливает печать на консоли после первого введения строки состояния двойной высоты. Но если я изменю задачу на что-то, что не изменит размер представленного фрейма, как, скажем, изменение его фона:

override func containerViewDidLayoutSubviews() {
    super.containerViewDidLayoutSubviews()
    presentedViewController.view.backgroundColor = UIColor.blue
    print("did layout")
}

метод никогда не ломается. По какой-либо причине изменение размера представленного фрейма представления в этом методе нарушает метод после первого переключения строки состояния двойной высоты. Я хотел бы объяснить это поведение. Вряд ли ошибка, но кажется, как один.

Несмотря на это, Apple заявляет, что, если авторазметка используется правильно, программисту ничего не нужно делать! Итак, мой вопрос: где или как мы должны предоставить представленные ограничения представления контроллера представления, чтобы он мог адаптироваться к его временному контейнеру? Потому что, IMO, это проблема. Представленный контроллер представления принадлежит представлению переходного контейнера, которое является временным представлением, предоставленным UIKit, над которым мы не имеем большого доминирования. Если мы сможем привязать представленное представление к этому контейнеру, все проблемы будут решены. Но я никогда не видел, чтобы Apple делала это или даже говорила об этом.

Примечание. Явная привязка представленного представления к представлению контейнера в любом месте - в существующем контроллере аниматора (до анимации или в обработчике его завершения), в контроллере представления или в методе адаптивного делегата - не дает согласованных результатов, как упоминалось ранее.

ВЫВОДЫ: (1) Нет способа официально, правильно, чисто или последовательно обрабатывать строку состояния двойной высоты с модальными презентациями; (2) Apple испортила его с помощью строки состояния двойной высоты и не может дождаться того дня, когда на всех iPhone появится метка на экране.

Ответы

Ответ 1

Мой ответ: вы не должны использовать ограничения в случае пользовательских модальных презентаций

Поэтому я знаю вашу боль, поэтому постараюсь помочь вам сэкономить время и усилия, предоставив некоторые подсказки, которые я неожиданно обнаружил.


Пример дела:

Анимация пользовательского интерфейса карты выглядит следующим образом:

enter image description here

Условия дальнейшего использования:

  • Родитель - UIViewController с элементом кнопки панели "Детали"
  • Ребенок - UIViewController с "Другой"

Проблемы, о которых вы упомянули, начались, когда моя анимация включала изменение размера вместе с движением. Это вызывает различные виды эффектов, в том числе:

  • Родительская область в строке состояния появилась и исчезла
  • Родительские подпредставления плохо анимировались - прыжки, дублирование и прочие глюки.

После нескольких дней отладки и поиска я нашел следующее решение (извините за некоторые магические числа;)):

UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       delay: 0,
                       usingSpringWithDamping: 1,
                       initialSpringVelocity: 0.4,
                       options: .curveEaseIn, animations: {
            toVC.view.transform = CGAffineTransform(translationX: 0, y: self.finalFrame.minY)
            toVC.view.frame = self.finalFrame
            toVC.view.layer.cornerRadius = self.cornerRadius

            fromVC.view.layer.cornerRadius = self.cornerRadius
            var transform = CATransform3DIdentity
            transform = CATransform3DScale(transform, scale, scale, 1.0)
            transform = CATransform3DTranslate(transform, 0, wdiff, 0)
            fromVC.view.layer.transform = transform
            fromVC.view.alpha = 0.6
        }) { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }

Главное, что вы должны использовать CGAffineTransform3D чтобы избежать проблем с анимацией и проблем с анимацией подпредставлений (2D-преобразования не работают по неизвестным причинам).

Этот подход исправляет, я надеюсь, все ваши проблемы без использования ограничений.

Не стесняйтесь задавать вопросы.

UPD: согласно строке состояния In-Call

После нескольких часов всевозможных экспериментов и изучения подобных проектов, подобных этому и этому, и вопросов о стекопереработке, подобных этому, этому (на самом деле это весело, ответ ОП есть) и тому подобное, я полностью запутался. Похоже, мое решение обрабатывает двойную строку состояния на уровне UIKit (она корректно корректируется), но это же движение игнорирует предыдущие преобразования. Причина неизвестна.


Образцы кода:

Вы можете увидеть рабочее решение здесь, на Github

PS Я не уверен, если это нормально, чтобы опубликовать ссылку GitHub в ответе. Буду признателен за совет, как разместить 100-300 строк кода в ответе.

Ответ 2

Я боролся с statusBar двойной высоты в моем текущем проекте, и мне удалось решить почти каждую проблему (последняя оставшаяся проблема - очень странная проблема преобразования, когда presentingViewController встроен в UITabBarController).

При изменении высоты строки состояния отправляется уведомление.
Ваш подкласс UIPresentationController должен подписаться на это конкретное уведомление и настроить фрейм containerView и его подпредставлений:

UIApplication.willChangeStatusBarFrameNotification

Вот пример кода, который я использую:

final class MyCustomPresentationController: UIPresentationController {

    // MARK: - StatusBar

    private func subscribeToStatusBarNotifications() {
        let notificationName = UIApplication.willChangeStatusBarFrameNotification
        NotificationCenter.default.addObserver(self, selector: #selector(statusBarWillChangeFrame(notification:)), name: notificationName, object: nil)
    }

    @objc private func statusBarWillChangeFrame(notification: Notification?) {
        if let newFrame = notification?.userInfo?[UIApplication.statusBarFrameUserInfoKey] as? CGRect {
            statusBarWillChangeFrame(to: newFrame)
        } else {
            statusBarWillChangeFrame(to: .zero)
        }
    }

    func statusBarWillChangeFrame(to newFrame: CGRect) {
        layoutContainerView(animated: true)
    }

    // MARK: - Object Lifecycle

    deinit {
        // Unsubscribe from all notifications
        NotificationCenter.default.removeObserver(self)
    }

    // MARK: - Layout

    /// Called when the status-bar is about to change its frame.
    /// Relayout the containerView and its subviews
    private func layoutContainerView(animated: Bool) {
        guard let containerView = self.containerView else { return }

        // Retrieve informations about status-bar
        let statusBarHeight = UIApplication.shared.statusBarFrame.height
        let normalStatusBarHeight = Constants.Number.statusBarNormalHeight // 20
        let isStatusBarNormal = statusBarHeight ==~ normalStatusBarHeight

        if animated {
            containerView.frame = …
            updatePresentedViewFrame(animated: true)
        } else {
            // Update containerView frame
            containerView.frame = …
            updatePresentedViewFrame(animated: false)
        }
    }

    func updatePresentedViewFrame(animated: Bool) {
        self.presentedView?.frame = …
    }
}

result image