Is there a public API for card view UI that can be seen across iOS 10?

后端 未结 4 856
盖世英雄少女心
盖世英雄少女心 2020-12-07 09:06

The Music app in iOS 10 adopts a new card-like appearance: Now Playing screen slides up, while the view below in the hierarchy zooms out, protruding slightly at the top of t

相关标签:
4条回答
  • 2020-12-07 09:16

    You can build the segue in interface builder. Selecting modal segue from ViewController to CardViewController.

    For your CardViewController :

    import UIKit
    
    class CardViewController: UIViewController {
    
      required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
      }
    
      override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: Bundle!)  {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    
        self.commonInit()
      }
    
      func commonInit() {
        self.modalPresentationStyle = .custom
        self.transitioningDelegate = self
      }
    
      override func viewDidLoad() {
        super.viewDidLoad()
    
        roundViews()
      }
    
      func roundViews() {
        view.layer.cornerRadius = 8
        view.clipsToBounds = true
      }
    
    }
    

    then add this extension:

    extension CardViewController: UIViewControllerTransitioningDelegate {
    
      func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        if presented == self {
          return CardPresentationController(presentedViewController: presented, presenting: presenting)
        }
        return nil
      }
    
      func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if presented == self {
          return CardAnimationController(isPresenting: true)
        } else {
          return nil
        }
      }
    
      func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if dismissed == self {
          return CardAnimationController(isPresenting: false)
        } else {
          return nil
        }
      }
    
    }
    

    Finally, you will need 2 more classes:

    import UIKit
    
    class CardPresentationController: UIPresentationController {
    
      lazy var dimmingView :UIView = {
        let view = UIView(frame: self.containerView!.bounds)
        view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.3)
        view.layer.cornerRadius = 8
        view.clipsToBounds = true
        return view
      }()
    
      override func presentationTransitionWillBegin() {
    
        guard
          let containerView = containerView,
          let presentedView = presentedView
          else {
            return
        }
    
        // Add the dimming view and the presented view to the heirarchy
        dimmingView.frame = containerView.bounds
        containerView.addSubview(dimmingView)
        containerView.addSubview(presentedView)
    
        // Fade in the dimming view alongside the transition
        if let transitionCoordinator = self.presentingViewController.transitionCoordinator {
          transitionCoordinator.animate(alongsideTransition: {(context: UIViewControllerTransitionCoordinatorContext!) -> Void in
            self.dimmingView.alpha = 1.0
          }, completion:nil)
        }
      }
    
      override func presentationTransitionDidEnd(_ completed: Bool)  {
        // If the presentation didn't complete, remove the dimming view
        if !completed {
          self.dimmingView.removeFromSuperview()
        }
      }
    
      override func dismissalTransitionWillBegin()  {
        // Fade out the dimming view alongside the transition
        if let transitionCoordinator = self.presentingViewController.transitionCoordinator {
          transitionCoordinator.animate(alongsideTransition: {(context: UIViewControllerTransitionCoordinatorContext!) -> Void in
            self.dimmingView.alpha  = 0.0
          }, completion:nil)
        }
      }
    
      override func dismissalTransitionDidEnd(_ completed: Bool) {
        // If the dismissal completed, remove the dimming view
        if completed {
          self.dimmingView.removeFromSuperview()
        }
      }
    
      override var frameOfPresentedViewInContainerView : CGRect {
    
        // We don't want the presented view to fill the whole container view, so inset it's frame
        let frame = self.containerView!.bounds;
        var presentedViewFrame = CGRect.zero
        presentedViewFrame.size = CGSize(width: frame.size.width, height: frame.size.height - 40)
        presentedViewFrame.origin = CGPoint(x: 0, y: 40)
    
        return presentedViewFrame
      }
    
      override func viewWillTransition(to size: CGSize, with transitionCoordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: transitionCoordinator)
    
        guard
          let containerView = containerView
          else {
            return
        }
    
        transitionCoordinator.animate(alongsideTransition: {(context: UIViewControllerTransitionCoordinatorContext!) -> Void in
          self.dimmingView.frame = containerView.bounds
        }, completion:nil)
      }
    
    }
    

    and:

    import UIKit
    
    
    class CardAnimationController: NSObject {
    
      let isPresenting :Bool
      let duration :TimeInterval = 0.5
    
      init(isPresenting: Bool) {
        self.isPresenting = isPresenting
    
        super.init()
      }
    }
    
    // MARK: - UIViewControllerAnimatedTransitioning
    
    extension CardAnimationController: UIViewControllerAnimatedTransitioning {
    
      func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return self.duration
      }
    
      func animateTransition(using transitionContext: UIViewControllerContextTransitioning)  {
        let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        let fromView = fromVC?.view
        let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        let toView = toVC?.view
    
        let containerView = transitionContext.containerView
    
        if isPresenting {
          containerView.addSubview(toView!)
        }
    
        let bottomVC = isPresenting ? fromVC : toVC
        let bottomPresentingView = bottomVC?.view
    
        let topVC = isPresenting ? toVC : fromVC
        let topPresentedView = topVC?.view
        var topPresentedFrame = transitionContext.finalFrame(for: topVC!)
        let topDismissedFrame = topPresentedFrame
        topPresentedFrame.origin.y -= topDismissedFrame.size.height
        let topInitialFrame = topDismissedFrame
        let topFinalFrame = isPresenting ? topPresentedFrame : topDismissedFrame
        topPresentedView?.frame = topInitialFrame
    
        UIView.animate(withDuration: self.transitionDuration(using: transitionContext),
                       delay: 0,
                       usingSpringWithDamping: 300.0,
                       initialSpringVelocity: 5.0,
                       options: [.allowUserInteraction, .beginFromCurrentState], //[.Alert, .Badge]
          animations: {
            topPresentedView?.frame = topFinalFrame
            let scalingFactor : CGFloat = self.isPresenting ? 0.92 : 1.0
            bottomPresentingView?.transform = CGAffineTransform.identity.scaledBy(x: scalingFactor, y: scalingFactor)
    
        }, completion: {
          (value: Bool) in
          if !self.isPresenting {
            fromView?.removeFromSuperview()
          }
        })
    
    
        if isPresenting {
          animatePresentationWithTransitionContext(transitionContext)
        } else {
          animateDismissalWithTransitionContext(transitionContext)
        }
      }
    
      func animatePresentationWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) {
    
        let containerView = transitionContext.containerView
        guard
          let presentedController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
          let presentedControllerView = transitionContext.view(forKey: UITransitionContextViewKey.to)
          else {
            return
        }
    
        // Position the presented view off the top of the container view
        presentedControllerView.frame = transitionContext.finalFrame(for: presentedController)
        presentedControllerView.center.y += containerView.bounds.size.height
    
        containerView.addSubview(presentedControllerView)
    
        // Animate the presented view to it's final position
        UIView.animate(withDuration: self.duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .allowUserInteraction, animations: {
          presentedControllerView.center.y -= containerView.bounds.size.height
        }, completion: {(completed: Bool) -> Void in
          transitionContext.completeTransition(completed)
        })
      }
    
      func animateDismissalWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) {
    
        let containerView = transitionContext.containerView
        guard
          let presentedControllerView = transitionContext.view(forKey: UITransitionContextViewKey.from)
          else {
            return
        }
    
        // Animate the presented view off the bottom of the view
        UIView.animate(withDuration: self.duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .allowUserInteraction, animations: {
          presentedControllerView.center.y += containerView.bounds.size.height
        }, completion: {(completed: Bool) -> Void in
          transitionContext.completeTransition(completed)
        })
      }
    }
    

    Finally, in order to animate the CardViewController closing, hook your closing button to FirstResponder selecting dismiss and add this method to ViewController:

    func dismiss(_ segue: UIStoryboardSegue) {
        self.dismiss(animated: true, completion: nil)
    }
    
    0 讨论(0)
  • 2020-12-07 09:31

    Apple show how to do this using UIViewPropertyAnimator in WWDC 2017 Session 230: Advanced Animations with UIKit

    The basic idea is that you add a child view controller, and position it mostly off-screen. When tapped/panned you animate the child view controller's frame.

    import UIKit
    
    class CardViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.backgroundColor = .red
        }
    }
    
    class ViewController: UIViewController {
        private let cardViewController = CardViewController()
        private var cardHiddenConstraint: NSLayoutConstraint!
        private var cardVisibleConstraint: NSLayoutConstraint!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            addChild(cardViewController)
            let cardViewControllerView = cardViewController.view!
            cardViewControllerView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(cardViewControllerView)
    
            cardHiddenConstraint = cardViewControllerView.topAnchor.constraint(equalTo: view.bottomAnchor, constant: -50)
            cardVisibleConstraint = cardViewControllerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 50)
    
            let cardViewControllerViewConstraints = [
                cardViewControllerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                cardViewControllerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                cardHiddenConstraint!,
                cardViewControllerView.heightAnchor.constraint(equalTo: view.heightAnchor)
            ]
            NSLayoutConstraint.activate(cardViewControllerViewConstraints)
    
            cardViewController.didMove(toParent: self)
    
            let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
            cardViewController.view.addGestureRecognizer(tapGestureRecognizer)
        }
    
        @objc private func handleTapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
            let frameAnimator = UIViewPropertyAnimator(duration: 0.3, dampingRatio: 1) {
                if self.cardHiddenConstraint.isActive {
                    self.cardHiddenConstraint.isActive = false
                    self.cardVisibleConstraint.isActive = true
                } else {
                    self.cardVisibleConstraint.isActive = false
                    self.cardHiddenConstraint.isActive = true
                }
                self.view.layoutIfNeeded()
            }
            frameAnimator.startAnimation()
        }
    }
    
    0 讨论(0)
  • 2020-12-07 09:31

    If you are happy to add a third party dependency, then try SPStorkController, it's up to date (Swift 4.2 at time of writing this) and works with minimal configuration.

    0 讨论(0)
  • 2020-12-07 09:36

    Ok, I'll try to give you a compact solution with a minimum of code.

    Fast solution. You need to present a controller modally with modalPresentationStyle-property set to .overCurrentContext. You can set the value before preset(controller:...)-method get called or in prepare(for:...)-one if it's a segue transition. For sliding up use modalTransitionStyle set to .coverVertical.

    To "zoom out" source view just update its bounds in viewWill(Diss)appear-methods. In most of cases this will work.

    Don't forget to set your modal controller background view transparent so the underlying view still be visible.

    Sliding up/down smoothly. You need to setup a transition between the controllers in a proper way. If you look closer to Apple music app, you'll see a way to hide top controller with slide down gesture. You can customise your view (dis)appearance too. Take a look at this article. It uses UIKit-methods only. Unfortunately this way requires lots of code, but you can use 3rd party libraries to setup transitions. Like this one.

    0 讨论(0)
提交回复
热议问题