UIPresentationController в стиле iOS 13, не полагаясь на снимки?

В iOS 13, похоже, используется новый UIPresentationController для представления контроллеров модального представления, но тот, который не полагается на создание снимков представленного контроллера представления (как это делают большинство / все библиотеки). Контроллер представления представления является «живым» и продолжает отображать анимацию/изменения, в то время как контроллер модального представления отображается поверх прозрачного/тонированного фона.

Я могу легко воспроизвести это (поскольку цель состоит в том, чтобы сделать обратно совместимую версию для iOS 10/11/12 и т. д.), используя CGAffineTransform в представлении контроллера представления, однако часто при вращении устройства представление представления начинается деформироваться и расти за пределы, по-видимому, потому, что система обновляет свой frame, пока к нему применяется активный transform.

Согласно документации, frame не определено, когда к представлению применяется transform. Учитывая, что система, похоже, изменяет фрейм, а не я, как мне решить эту проблему, не прибегая к хакерским решениям, в которых я обновляю границы представления представления? Мне нужно, чтобы этот контроллер презентации оставался универсальным, поскольку контроллер представления может быть любой формы и формы и не обязательно будет полноэкранным представлением.

Вот что у меня есть до сих пор - это простой подкласс UIPresentationController, который, кажется, работает нормально, однако вращение устройства, а затем отклонение представленного контроллера представления, кажется, деформирует границы представленного контроллера представления (становится слишком широким или сжимается, в зависимости от того, вы представили модальный контроллер в альбомной/портретной ориентации)

class SheetPresentationController: UIPresentationController {
  override var frameOfPresentedViewInContainerView: CGRect {
    return CGRect(x: 40, y: containerView!.bounds.height / 2, width: containerView!.bounds.width-80, height: containerView!.bounds.height / 2)
  }

  override func containerViewWillLayoutSubviews() {
    super.containerViewWillLayoutSubviews()

    if let _ = presentingViewController.transitionCoordinator {
      // We're transitioning - don't touch the frame yet as it'll
      // clash with our transform
    } else {
      self.presentedView?.frame = self.frameOfPresentedViewInContainerView
    }
  }

  override func presentationTransitionWillBegin() {
    super.presentationTransitionWillBegin()

    containerView?.backgroundColor = .clear

    if let coordinator = presentingViewController.transitionCoordinator {
      coordinator.animate(alongsideTransition: { [weak self] _ in
        self?.containerView?.backgroundColor = UIColor.black.withAlphaComponent(0.3)

        // Scale the presenting view
        self?.presentingViewController.view.layer.cornerRadius = 16

        self?.presentingViewController.view.transform = CGAffineTransform.init(scaleX: 0.9, y: 0.9)
        }, completion: nil)
    }
  }

  override func dismissalTransitionWillBegin() {
    if let coordinator = presentingViewController.transitionCoordinator {
      coordinator.animate(alongsideTransition: { [weak self] _ in
        self?.containerView?.backgroundColor = .clear

        self?.presentingViewController.view.layer.cornerRadius = 0
        self?.presentingViewController.view.transform = .identity
        }, completion: nil)
    }
  }
}

И контроллер Presenting Animation:

import UIKit

final class PresentingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    guard let presentedViewController = transitionContext.viewController(forKey: .to) else {
      return
    }

    let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
    let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)

    let containerView = transitionContext.containerView
    containerView.addSubview(presentedViewController.view)

    let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)
    presentedViewController.view.frame = finalFrameForPresentedView

    // Move it below the screen so it slides up
    presentedViewController.view.frame.origin.y = containerView.bounds.height

    animator.addAnimations {
      presentedViewController.view.frame = finalFrameForPresentedView      
    }

    animator.addCompletion { (animationPosition) in
      if animationPosition == .end {
        transitionContext.completeTransition(true)
      }
    }

    animator.startAnimation()
  }

  func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.6
  }
}

А также отклоняющий контроллер анимации:

import UIKit

final class DismissingAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    guard let presentedViewController = transitionContext.viewController(forKey: .from) else {
      return
    }

    guard let presentingViewController = transitionContext.viewController(forKey: .to) else {
      return
    }

    let finalFrameForPresentedView = transitionContext.finalFrame(for: presentedViewController)

    let containerView = transitionContext.containerView
    let offscreenFrame = CGRect(x: finalFrameForPresentedView.minX, y: containerView.bounds.height, width: finalFrameForPresentedView.width, height: finalFrameForPresentedView.height)

    let springTiming = UISpringTimingParameters(dampingRatio: 1.0, initialVelocity: CGVector(dx:1.0, dy: 1.0))
    let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: transitionDuration(using: transitionContext), timingParameters: springTiming)

    animator.addAnimations {
      presentedViewController.view.frame = offscreenFrame
    }

    animator.addCompletion { (position) in
      if position == .end {
        // Complete transition        
        transitionContext.completeTransition(true)
      }
    }

    animator.startAnimation()
  }

  func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.6
  }
}
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
3 145
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Пользовательские презентации — сложная часть UIKit. Вот что приходит на ум, никаких гарантий ;-)

Я бы посоветовал вам либо попытаться «зафиксировать» анимацию в представлении представления, поэтому в обратном вызове PresentationTransitionDidEnd(Bool) удалите преобразование и установите соответствующие ограничения для представления представления, которые соответствуют тому, что сделало преобразование. Или вы также можете просто анимировать изменения ограничения, чтобы имитировать преобразование.

Предположительно, вы получите обратный звонок viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator), чтобы управлять текущей презентацией, если произойдет ротация.

Да, это «хакерские» решения, которых я пытаюсь избежать. Никакие ограничения не могут имитировать красивое масштабирование, которое вы получаете с помощью преобразования CALayer/View, так что это невозможно. Я также не хочу возиться с рамкой представления представления, потому что ведущий не владеет им и не должен предполагать, какую форму/размер/форму принимает представление представления.

strangetimes 16.07.2019 19:01

Справедливости ради, iOS 13 делает это очень хорошо, поэтому я уверен, что из этого есть более чистый выход. Возможно, они перемещают представление представления в промежуточное представление, которым они владеют, и масштабируют его, а затем возвращают обратно, когда закончат. Трудно сказать.

strangetimes 17.07.2019 13:35
Ответ принят как подходящий

Хорошо, я понял это. Кажется, iOS 13 НЕ использует масштабное преобразование. В тот момент, когда вы это сделаете, как объяснялось, вращение устройства изменит рамку представления представления, и, поскольку вы уже применили transform к представлению, размер представления изменится неожиданным образом, и преобразование больше не будет действительным.

Решение состоит в том, чтобы вместо этого использовать перспективу по оси Z, которая даст вам точно такой же результат, но при этом выдержит повороты и т. д., поскольку все, что вы делаете, это перемещаете вид обратно в трехмерное пространство (ось Z), таким образом эффективно уменьшение масштаба. Вот преобразование, которое сделало это для меня (Swift):

  func calculatePerspectiveTransform() -> CATransform3D {
    let eyePosition:Float = 10.0;
    var contentTransform:CATransform3D = CATransform3DIdentity
    contentTransform.m34 = CGFloat(-1/eyePosition)
    contentTransform = CATransform3DTranslate(contentTransform, 0, 0, -2)
    return contentTransform
  }

Вот статья, объясняющая, как это работает: https://whackylabs.com/uikit/2014/10/29/add-some-perspective-to-your-uiviews/

В вашем UIPresenterController вам также нужно будет сделать следующее, чтобы правильно обрабатывать это преобразование при поворотах:

  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    // Reset transform before we rotate and then apply it again during rotation
    if let presentingView = presentingViewController.view {
      presentingView.layer.transform = CATransform3DIdentity
    }

    coordinator.animate(alongsideTransition: { [weak self] (context) in
      if let presentingView = self?.presentingViewController.view {
        presentingView.layer.transform = self?.calculatePerspectiveTransform() ?? CATransform3DIdentity
      }
    })
  }

Другие вопросы по теме