试图模仿 Mail.app 组合动画,使图层保持可见
Trying to mimic the Mail.app compose animation keeping a layer in view
我已经尝试了一段时间,但我不知道如何创建在 iOS 10+
中看到的撰写动画,当您可以将新撰写的电子邮件向下拖动时,它会停留在底部,其余部分正常访问该应用程序,然后当您点击它时,它会重新显示。
我创建了一个示例项目,其中我有一个 UIViewController
,它显示另一个 UIViewController
,其中有一个 UIPanGestureRecognizer
,其中 UINavigationController
触发 pangesture
状态分析器。
我确实可以拖动以关闭它,但我找不到保持框架的方法。
下面是我正在尝试完成的打印屏幕,然后是我使用的代码到我卡住的地方。
UIViewController
也就是presentingViewController
class
//
// ViewController.swift
// dismissLayerTest
//
// Created by Ivan Cantarino on 27/09/17.
// Copyright © 2017 Ivan Cantarino. All rights reserved.
//
import UIKit
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
@objc let interactor = Interactor()
lazy var presentButton: UIButton = {
let b = UIButton(type: .custom)
b.setTitle("Present", for: .normal)
b.setTitleColor(.black, for: .normal)
b.addTarget(self, action: #selector(didTapPresentButton), for: .touchUpInside)
return b
}()
lazy var testbutton: UIButton = {
let b = UIButton(type: .custom)
b.setTitle("test", for: .normal)
b.setTitleColor(.black, for: .normal)
b.addTarget(self, action: #selector(test), for: .touchUpInside)
return b
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = .white
view.addSubview(presentButton)
presentButton.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 100, height: 100)
presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
view.addSubview(testbutton)
testbutton.anchor(top: nil, left: nil, bottom: presentButton.topAnchor, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 100, paddingRight: 0, width: 100, height: 100)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@objc func didTapPresentButton() {
let presentedVC = PresentedViewController()
let navController = UINavigationController(rootViewController: presentedVC)
navController.transitioningDelegate = self
presentedVC.interactor = interactor // new
navController.modalPresentationStyle = .custom
navController.view.layer.masksToBounds = true
present(navController, animated: true, completion: nil)
}
@objc func test() {
print("test")
}
// Handles the presenting animation
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomAnimationForPresentor()
}
// Handles the dismissing animation
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomAnimationForDismisser()
}
// interaction controller, only for dismissing the view;
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
// delegate do custom modal presentation style
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return CustomPresentationController(presentedViewController: presented, presenting: presenting)
}
}
UIViewController
2即presentedViewController
import Foundation
import UIKit
class PresentedViewController: UIViewController, UIViewControllerTransitioningDelegate, UIGestureRecognizerDelegate {
@objc var interactor: Interactor? = nil
@objc var panGr = UIPanGestureRecognizer()
@objc var panTapRecon = UITapGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
let leftB = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel))
navigationItem.leftBarButtonItem = leftB
panGr = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
navigationController?.navigationBar.addGestureRecognizer(panGr)
panTapRecon = UITapGestureRecognizer(target: self, action: #selector(handleNavControllerTapGR))
navigationController?.navigationBar.addGestureRecognizer(panTapRecon)
}
@objc func didTapCancel() {
guard let interactor = interactor else { return }
interactorFinish(interactor: interactor)
dismiss(animated: true, completion: nil)
}
@objc func handleNavControllerTapGR(_ sender: UITapGestureRecognizer) {
print("tap detected")
}
// Swipe gesture recognizer handler
@objc func handleGesture(_ sender: UIPanGestureRecognizer) {
//percentThreshold: This variable sets how far down the user has to drag
//in order to trigger the modal dismissal. In this case, it’s set to 40%.
let percentThreshold:CGFloat = 0.30
// convert y-position to downward pull progress (percentage)
let translation = sender.translation(in: view)
let verticalMovement = translation.y / view.bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard let interactor = interactor else { return }
switch sender.state {
case .began:
interactor.hasStarted = true
self.dismiss(animated: true, completion: nil)
case .changed:
// alterar se o tamanho do presentigViewController (MainTabBarController) for alterado no background
let scaleX = 0.95 + (progress * (1 - 0.95))
let scaleY = 0.95 + (progress * (1 - 0.95))
// Não deixa ultrapassar os 100% de scale (tamanho original)
if (scaleX > 1 && scaleY > 1) { return }
presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: scaleX, y: scaleY);
presentingViewController?.view.layer.masksToBounds = true
interactor.shouldFinish = progress > percentThreshold
interactor.update(progress)
case .cancelled:
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
if (interactor.shouldFinish) {
interactorFinish(interactor: interactor)
} else {
// repõe o MainTabBarController na posição dele atrás do NewPostController
UIView.animate(withDuration: 0.5, animations: {
self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 0.95, y: 0.95);
self.presentingViewController?.view.layer.masksToBounds = true
let c = UIColor.black.withAlphaComponent(0.4)
let shadowView = self.presentingViewController?.view.viewWithTag(999)
shadowView?.backgroundColor = c
})
interactor.cancel()
}
default: break
}
}
@objc func interactorFinish(interactor: Interactor) {
removeShadow()
interactor.finish()
}
// remove a shadow view
@objc func removeShadow() {
UIView.animate(withDuration: 0.2, animations: {
self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 1.0, y: 1.0);
self.presentingViewController?.view.layer.masksToBounds = true
}) { _ in
}
}
}
这是一个包含自定义演示文稿的帮助文件:
//
// Helper.swift
// dismissLayerTest
//
// Created by Ivan Cantarino on 27/09/17.
// Copyright © 2017 Ivan Cantarino. All rights reserved.
//
import Foundation
import UIKit
class Interactor: UIPercentDrivenInteractiveTransition {
@objc var hasStarted = false
@objc var shouldFinish = false
}
extension UIView {
@objc func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddinfLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddinfLeft).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
if let right = right {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
if width != 0 {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if height != 0 {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
@objc func roundCorners(corners:UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
}
class CustomAnimationForDismisser: NSObject, UIViewControllerAnimatedTransitioning {
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.27
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {
print("Returning animateTransition VC")
return
}
// from view só existe no dismiss
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else {
print("Failed to instantiate fromView: CustomAnimationForDismisser()")
return
}
// Set up some variables for the animation.
let containerFrame: CGRect = containerView.frame
var fromViewFinalFrame: CGRect = transitionContext.finalFrame(for: fromVC)
fromViewFinalFrame = CGRect(x: 0, y: containerFrame.size.height, width: containerFrame.size.width, height: containerFrame.size.height)
// Animate using the animator's own duration value.
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: {
fromView.frame = fromViewFinalFrame
}) { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
}
}
}
class CustomAnimationForPresentor: NSObject, UIViewControllerAnimatedTransitioning {
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
// obtém os VCs para não o perder na apresentação (default desaparece por trás)
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {//, let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
print("Returning animateTransition VC")
return
}
// gets the view of the presented object
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { return }
// Set up animation parameters.
toView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height)
// Always add the "to" view to the container.
containerView.addSubview(toView)
// Animate using the animator's own duration value.
UIView.animate(withDuration: 0.35, delay: 0, options: .curveEaseOut, animations: {
// Zooms out da MainTabBarController - o VC
fromVC.view.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
// propriedades declaradas no CustomPresentationController() // Anima o presented view
toView.transform = .identity
}, completion: { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// So it avoids view stacks and overlap issues
if (!success) { toView.removeFromSuperview() }
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
})
}
}
class CustomPresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
// Tamanho desejado para o NewPostController
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerBounds = containerView?.bounds else {
print("Failed to instantiate container bounds: CustomPresentationController")
return .zero
}
return CGRect(x: 0.0, y: 0.0, width: containerBounds.width, height: containerBounds.height)
}
// Garante que o frame do view controller a mostrar, se mantém conforme desenhado na função frameOfPresentedViewInContainerView
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
}
这种效果也可以在其他应用程序中看到,例如 Music app
、Stack Exchange/Overflow iOS App
有没有人知道如何实现这一点?我觉得我真的很接近实现它,但我找不到在屏幕上保留带有图层的 dismissed
视图的方法。
上面的项目可以找到here
非常感谢。
问候。
我建议 Apple(在您提供的动画屏幕 gif 中非常有帮助)不使用呈现的视图控制器。如果是这样,呈现的视图控制器将无法缩小其视图——并且在关闭时,呈现的视图控制器的视图将完全消失。
我想说这个接口的底层是一个带有多个子视图控制器的父视图控制器(或者可能只是一个带有两个子视图的普通视图控制器)。因此,我们可以随心所欲地显示这两个子视图。您的 gif 动画显示了两个子视图的两种可能排列方式:重叠,一个在另一个之上,第二个视图从屏幕底部几乎看不到。
我已经尝试了一段时间,但我不知道如何创建在 iOS 10+
中看到的撰写动画,当您可以将新撰写的电子邮件向下拖动时,它会停留在底部,其余部分正常访问该应用程序,然后当您点击它时,它会重新显示。
我创建了一个示例项目,其中我有一个 UIViewController
,它显示另一个 UIViewController
,其中有一个 UIPanGestureRecognizer
,其中 UINavigationController
触发 pangesture
状态分析器。
我确实可以拖动以关闭它,但我找不到保持框架的方法。
下面是我正在尝试完成的打印屏幕,然后是我使用的代码到我卡住的地方。
UIViewController
也就是presentingViewController
class
//
// ViewController.swift
// dismissLayerTest
//
// Created by Ivan Cantarino on 27/09/17.
// Copyright © 2017 Ivan Cantarino. All rights reserved.
//
import UIKit
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
@objc let interactor = Interactor()
lazy var presentButton: UIButton = {
let b = UIButton(type: .custom)
b.setTitle("Present", for: .normal)
b.setTitleColor(.black, for: .normal)
b.addTarget(self, action: #selector(didTapPresentButton), for: .touchUpInside)
return b
}()
lazy var testbutton: UIButton = {
let b = UIButton(type: .custom)
b.setTitle("test", for: .normal)
b.setTitleColor(.black, for: .normal)
b.addTarget(self, action: #selector(test), for: .touchUpInside)
return b
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = .white
view.addSubview(presentButton)
presentButton.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 100, height: 100)
presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
view.addSubview(testbutton)
testbutton.anchor(top: nil, left: nil, bottom: presentButton.topAnchor, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 100, paddingRight: 0, width: 100, height: 100)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@objc func didTapPresentButton() {
let presentedVC = PresentedViewController()
let navController = UINavigationController(rootViewController: presentedVC)
navController.transitioningDelegate = self
presentedVC.interactor = interactor // new
navController.modalPresentationStyle = .custom
navController.view.layer.masksToBounds = true
present(navController, animated: true, completion: nil)
}
@objc func test() {
print("test")
}
// Handles the presenting animation
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomAnimationForPresentor()
}
// Handles the dismissing animation
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomAnimationForDismisser()
}
// interaction controller, only for dismissing the view;
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
// delegate do custom modal presentation style
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return CustomPresentationController(presentedViewController: presented, presenting: presenting)
}
}
UIViewController
2即presentedViewController
import Foundation
import UIKit
class PresentedViewController: UIViewController, UIViewControllerTransitioningDelegate, UIGestureRecognizerDelegate {
@objc var interactor: Interactor? = nil
@objc var panGr = UIPanGestureRecognizer()
@objc var panTapRecon = UITapGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
let leftB = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel))
navigationItem.leftBarButtonItem = leftB
panGr = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
navigationController?.navigationBar.addGestureRecognizer(panGr)
panTapRecon = UITapGestureRecognizer(target: self, action: #selector(handleNavControllerTapGR))
navigationController?.navigationBar.addGestureRecognizer(panTapRecon)
}
@objc func didTapCancel() {
guard let interactor = interactor else { return }
interactorFinish(interactor: interactor)
dismiss(animated: true, completion: nil)
}
@objc func handleNavControllerTapGR(_ sender: UITapGestureRecognizer) {
print("tap detected")
}
// Swipe gesture recognizer handler
@objc func handleGesture(_ sender: UIPanGestureRecognizer) {
//percentThreshold: This variable sets how far down the user has to drag
//in order to trigger the modal dismissal. In this case, it’s set to 40%.
let percentThreshold:CGFloat = 0.30
// convert y-position to downward pull progress (percentage)
let translation = sender.translation(in: view)
let verticalMovement = translation.y / view.bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard let interactor = interactor else { return }
switch sender.state {
case .began:
interactor.hasStarted = true
self.dismiss(animated: true, completion: nil)
case .changed:
// alterar se o tamanho do presentigViewController (MainTabBarController) for alterado no background
let scaleX = 0.95 + (progress * (1 - 0.95))
let scaleY = 0.95 + (progress * (1 - 0.95))
// Não deixa ultrapassar os 100% de scale (tamanho original)
if (scaleX > 1 && scaleY > 1) { return }
presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: scaleX, y: scaleY);
presentingViewController?.view.layer.masksToBounds = true
interactor.shouldFinish = progress > percentThreshold
interactor.update(progress)
case .cancelled:
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
if (interactor.shouldFinish) {
interactorFinish(interactor: interactor)
} else {
// repõe o MainTabBarController na posição dele atrás do NewPostController
UIView.animate(withDuration: 0.5, animations: {
self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 0.95, y: 0.95);
self.presentingViewController?.view.layer.masksToBounds = true
let c = UIColor.black.withAlphaComponent(0.4)
let shadowView = self.presentingViewController?.view.viewWithTag(999)
shadowView?.backgroundColor = c
})
interactor.cancel()
}
default: break
}
}
@objc func interactorFinish(interactor: Interactor) {
removeShadow()
interactor.finish()
}
// remove a shadow view
@objc func removeShadow() {
UIView.animate(withDuration: 0.2, animations: {
self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 1.0, y: 1.0);
self.presentingViewController?.view.layer.masksToBounds = true
}) { _ in
}
}
}
这是一个包含自定义演示文稿的帮助文件:
//
// Helper.swift
// dismissLayerTest
//
// Created by Ivan Cantarino on 27/09/17.
// Copyright © 2017 Ivan Cantarino. All rights reserved.
//
import Foundation
import UIKit
class Interactor: UIPercentDrivenInteractiveTransition {
@objc var hasStarted = false
@objc var shouldFinish = false
}
extension UIView {
@objc func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddinfLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddinfLeft).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
if let right = right {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
if width != 0 {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if height != 0 {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
@objc func roundCorners(corners:UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
}
class CustomAnimationForDismisser: NSObject, UIViewControllerAnimatedTransitioning {
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.27
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {
print("Returning animateTransition VC")
return
}
// from view só existe no dismiss
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else {
print("Failed to instantiate fromView: CustomAnimationForDismisser()")
return
}
// Set up some variables for the animation.
let containerFrame: CGRect = containerView.frame
var fromViewFinalFrame: CGRect = transitionContext.finalFrame(for: fromVC)
fromViewFinalFrame = CGRect(x: 0, y: containerFrame.size.height, width: containerFrame.size.width, height: containerFrame.size.height)
// Animate using the animator's own duration value.
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: {
fromView.frame = fromViewFinalFrame
}) { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
}
}
}
class CustomAnimationForPresentor: NSObject, UIViewControllerAnimatedTransitioning {
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
// obtém os VCs para não o perder na apresentação (default desaparece por trás)
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {//, let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
print("Returning animateTransition VC")
return
}
// gets the view of the presented object
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { return }
// Set up animation parameters.
toView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height)
// Always add the "to" view to the container.
containerView.addSubview(toView)
// Animate using the animator's own duration value.
UIView.animate(withDuration: 0.35, delay: 0, options: .curveEaseOut, animations: {
// Zooms out da MainTabBarController - o VC
fromVC.view.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
// propriedades declaradas no CustomPresentationController() // Anima o presented view
toView.transform = .identity
}, completion: { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// So it avoids view stacks and overlap issues
if (!success) { toView.removeFromSuperview() }
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
})
}
}
class CustomPresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
// Tamanho desejado para o NewPostController
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerBounds = containerView?.bounds else {
print("Failed to instantiate container bounds: CustomPresentationController")
return .zero
}
return CGRect(x: 0.0, y: 0.0, width: containerBounds.width, height: containerBounds.height)
}
// Garante que o frame do view controller a mostrar, se mantém conforme desenhado na função frameOfPresentedViewInContainerView
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
}
这种效果也可以在其他应用程序中看到,例如 Music app
、Stack Exchange/Overflow iOS App
有没有人知道如何实现这一点?我觉得我真的很接近实现它,但我找不到在屏幕上保留带有图层的 dismissed
视图的方法。
上面的项目可以找到here
非常感谢。 问候。
我建议 Apple(在您提供的动画屏幕 gif 中非常有帮助)不使用呈现的视图控制器。如果是这样,呈现的视图控制器将无法缩小其视图——并且在关闭时,呈现的视图控制器的视图将完全消失。
我想说这个接口的底层是一个带有多个子视图控制器的父视图控制器(或者可能只是一个带有两个子视图的普通视图控制器)。因此,我们可以随心所欲地显示这两个子视图。您的 gif 动画显示了两个子视图的两种可能排列方式:重叠,一个在另一个之上,第二个视图从屏幕底部几乎看不到。