代码之家  ›  专栏  ›  技术社区  ›  Ivan Cantarino

试图模仿邮件。应用程序合成动画,保持层在视图中

  •  2
  • Ivan Cantarino  · 技术社区  · 8 年前

    我已经尝试了一段时间,但我不知道如何创建在 iOS 10+ 当你可以向下拖动新撰写的电子邮件时,它会停留在底部,应用程序的其余部分正常访问,然后当你点击它时,它会重新显示。

    我创建了一个示例项目,其中我有一个 UIViewController 这是另一个 UIViewController 它有一个 UIPanGestureRecognizer UINavigationController 这点燃了 pangesture

    我确实可以拖拉它,但我找不到保持框架的方法。

    enter image description here

    UIViewController 这就是 presentingViewController

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

    非常感谢你。

    1 回复  |  直到 8 年前
        1
  •  1
  •   matt    8 年前

    我建议苹果(在你提供的动画屏幕gif中)不要使用显示视图控制器。如果是这样,则呈现视图控制器将无法缩小其视图,并且在取消时,呈现视图控制器的视图将完全消失。

    我想说,这个界面的底层是一个具有多个子视图控制器的父视图控制器(或者可能只是一个具有两个子视图的普通视图控制器)。因此,我们可以随时随地显示两个子视图。动画gif显示两个子视图的两种可能排列:重叠,一个在另一个上,第二个视图从屏幕底部几乎看不到。