简体   繁体   English

是否有可以在 iOS 10 中看到的卡片视图 UI 的公共 API?

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

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 the screen. iOS 10 中的音乐应用采用了新的卡片式外观:正在播放屏幕向上滑动,而层次结构中的下方视图缩小,在屏幕顶部略微突出。

音乐应用卡界面

Here is the example from Mail compose window:以下是邮件撰写窗口中的示例:

邮件撰写卡片界面

This metaphor can also be seen in Overcast, the popular podcast player:这个比喻也可以在流行的播客播放器 Overcast 中看到:

阴卡接口

Is there a function in UIKit for achieving this card-like appearance? UIKit 中是否有实现这种卡片式外观的功能?

You can build the segue in interface builder.您可以在界面构建器中构建转场。 Selecting modal segue from ViewController to CardViewController .选择从ViewControllerCardViewController模态转CardViewController

For your CardViewController :对于您的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:最后,您还需要 2 个类:

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 :最后,为了动画CardViewController关闭,将关闭按钮挂钩到FirstResponder选择dismiss并将此方法添加到ViewController

func dismiss(_ segue: UIStoryboardSegue) {
    self.dismiss(animated: true, completion: nil)
}

Apple show how to do this using UIViewPropertyAnimator in WWDC 2017 Session 230: Advanced Animations with UIKit Apple 在WWDC 2017 Session 230: Advanced Animations with UIKit 中展示了如何使用UIViewPropertyAnimator做到这一点

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

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 .您需要以模态方式呈现控制器, modalPresentationStyle - 属性设置为.overCurrentContext You can set the value before preset(controller:...) -method get called or in prepare(for:...) -one if it's a segue transition.您可以在调用preset(controller:...) -method 之前或在prepare(for:...) -one 之前设置值,如果它是 segue 转换。 For sliding up use modalTransitionStyle set to .coverVertical .对于向上滑动使用modalTransitionStyle设置为.coverVertical

To "zoom out" source view just update its bounds in viewWill(Diss)appear -methods.要“缩小”源视图,只需在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.如果您仔细观察 Apple 音乐应用程序,您会看到一种通过向下滑动手势隐藏顶部控制器的方法。 You can customise your view (dis)appearance too.您也可以自定义您的视图(dis)外观。 Take a look at this article .看看这篇文章 It uses UIKit -methods only.它仅使用UIKit方法。 Unfortunately this way requires lots of code, but you can use 3rd party libraries to setup transitions.不幸的是,这种方式需要大量代码,但您可以使用 3rd 方库来设置转换。 Like this one .这个

如果您愿意添加第三方依赖项,请尝试SPStorkController ,它是最新的(撰写本文时为 Swift 4.2)并且只需最少的配置即可使用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM