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
  }
}

person strangetimes    schedule 16.07.2019    source источник


Ответы (2)


Хорошо, я понял это. Кажется, 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
      }
    })
  }
person strangetimes    schedule 19.07.2019

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

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

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

person glotcha    schedule 16.07.2019
comment
Да, это «хакерские» решения, которых я пытаюсь избежать. Никакие ограничения не могут имитировать красивое масштабирование, которое вы получаете с помощью преобразования CALayer/View, так что это невозможно. Я также не хочу возиться с рамкой представления представления, потому что ведущий не владеет им и не должен предполагать, какую форму/размер/форму принимает представление представления. - person strangetimes; 16.07.2019
comment
Справедливости ради, iOS 13 делает это очень хорошо, поэтому я уверен, что из этого есть более чистый выход. Возможно, они перемещают представление представления в промежуточное представление, которым они владеют, и масштабируют его, а затем возвращают обратно, когда закончат. Трудно сказать. - person strangetimes; 17.07.2019