试图模仿Mail.app组合动画,保持视图中的图层

时间:2017-09-27 16:47:42

标签: ios swift3 swift4 uipresentationcontroller

我已经尝试了一段时间,但我无法弄清楚如何创建一个在iOS 10+中看到的Compose动画,当你可以向下拖动新的组合电子邮件,然后它保持在底部和其余部分应用程序通常被访问,然后当您点击它时,它会重新显示。

我创建了一个示例项目,其中我有一个UIViewController,其中显示另一个UIViewController UIPanGestureRecognizer,其中UINavigationController触发pangesture状态分析器。

我确实可以拖延来解雇它,但我找不到一种方法来保持框架。

Bellow有一个打印屏幕,显示我正在尝试完成的内容,然后是我用过的代码,我被困在哪里。

enter image description here

UIViewControllerpresentingViewController

//
//  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文件:

//
//  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 appStack Exchange/Overflow iOS App

有没有人暗示如何实现这一目标?我觉得我真的很接近实现它,但我找不到一种方法来保持dismissed视图在屏幕上有一层。

上面的项目可以找到here

非常感谢你。 问候。

1 个答案:

答案 0 :(得分:1)

我建议Apple(在你提供的动画屏幕gif中提供的帮助)不使用呈现的视图控制器。如果是,则呈现视图控制器将无法缩小其视图 - 并且在解雇时,呈现的视图控制器视图将完全消失。

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