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

iOS 10中的音乐应用程序采用了一种新的卡片式外观:“正在播放”屏幕向上滑动,而层次结构中的下方视图缩小,在屏幕顶部略微突出。

音乐应用卡界面

这是邮件撰写窗口中的示例:

邮件撰写卡界面

这个比喻也可以在stream行的播客播放器Overcast中看到:

阴天卡界面

在UIKit中是否有一个函数来实现这种卡片式的外观?

您可以在界面构build器中构buildsegue。 从ViewControllerselect模态segue到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 } } 

然后添加这个扩展名:

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

最后,你还需要两个类:

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

和:

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

最后,为了使CardViewControllerclosuresanimation,请将closuresbutton挂接到FirstResponderselectdismiss并将此方法添加到ViewController

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

这种UI可以使用自定义UIViewController转换 , UIPresentationControllerUIViewPropertyAnimator

示例应用程序: https : //github.com/peteog/CardUIExample

首先创build一个UIPresentationController子类。 这会:

  • 添加调光视图
  • 将显示视图控制器转换为从状态栏插入
  • 设置所呈现的视图的框架以使卡效果

码:

 import UIKit class PresentationController: UIPresentationController { private let dimmingView: UIView = { let dimmingView = UIView() dimmingView.backgroundColor = UIColor(white: 0, alpha: 0.5) dimmingView.alpha = 0 return dimmingView }() // MARK: UIPresentationController override func presentationTransitionWillBegin() { guard let containerView = containerView, let presentedView = presentedView else { return } dimmingView.frame = containerView.bounds containerView.addSubview(dimmingView) containerView.addSubview(presentedView) guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return } transitionCoordinator.animateAlongsideTransition(in: presentingViewController.view, animation: { _ in self.presentingViewController.view.transform = CGAffineTransform(scaleX: 0.94, y: 0.94) if !transitionCoordinator.isInteractive { (self.presentingViewController as? ViewController)?.statusBarStyle = .lightContent } }) transitionCoordinator.animate(alongsideTransition: { _ in self.dimmingView.alpha = 1.0 }) } override func presentationTransitionDidEnd(_ completed: Bool) { if !completed { dimmingView.removeFromSuperview() } if completed { (presentingViewController as? ViewController)?.statusBarStyle = .lightContent } } override func dismissalTransitionWillBegin() { guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return } transitionCoordinator.animate(alongsideTransition: { _ in self.dimmingView.alpha = 0 }) transitionCoordinator.animateAlongsideTransition(in: presentingViewController.view, animation: { _ in self.presentingViewController.view.transform = CGAffineTransform.identity if !transitionCoordinator.isInteractive { (self.presentingViewController as? ViewController)?.statusBarStyle = .default } }) } override func dismissalTransitionDidEnd(_ completed: Bool) { guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return } if transitionCoordinator.isCancelled { return } if completed { dimmingView.removeFromSuperview() (presentingViewController as? ViewController)?.statusBarStyle = .default } } override var frameOfPresentedViewInContainerView: CGRect { guard let containerView = containerView else { return .zero } var frame = containerView.bounds frame.size.height -= 40 frame.origin.y += 40 return frame } // MARK: UIViewController override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) guard let containerView = containerView else { return } coordinator.animate(alongsideTransition: { _ in self.dimmingView.frame = containerView.bounds }) } } 

接下来,我们需要一个将在两个屏幕之间进行animation的对象:

 import UIKit class AnimationController: NSObject, UIViewControllerAnimatedTransitioning { enum Direction { case present case dismiss } private let direction: Direction init(direction: Direction) { self.direction = direction super.init() } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.3 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let animator = interruptibleAnimator(using: transitionContext) animator.startAnimation() } func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { let duration = transitionDuration(using: transitionContext) let animator = UIViewPropertyAnimator(duration: duration, curve: .linear) let containerView = transitionContext.containerView let containerFrame = containerView.frame switch direction { case .present: guard let toViewController = transitionContext.viewController(forKey: .to), let toView = transitionContext.view(forKey: .to) else { fatalError() } var toViewStartFrame = transitionContext.initialFrame(for: toViewController) let toViewFinalFrame = transitionContext.finalFrame(for: toViewController) toViewStartFrame = toViewFinalFrame toViewStartFrame.origin.y = containerFrame.size.height - 44 toView.frame = toViewStartFrame animator.addAnimations { toView.frame = toViewFinalFrame } case .dismiss: guard let fromViewController = transitionContext.viewController(forKey: .from), let fromView = transitionContext.view(forKey: .from) else { fatalError() } var fromViewFinalFrame = transitionContext.finalFrame(for: fromViewController) fromViewFinalFrame.origin.y = containerFrame.size.height - 44 animator.addAnimations { fromView.frame = fromViewFinalFrame } } animator.addCompletion { finish in if finish == .end { transitionContext.finishInteractiveTransition() transitionContext.completeTransition(true) } else { transitionContext.cancelInteractiveTransition() transitionContext.completeTransition(false) } } return animator } } 

最后把它们全部挂在视图控制器中,添加手势识别器来控制交互转换。

 import UIKit class ViewController: UIViewController, UIViewControllerTransitioningDelegate { var statusBarStyle: UIStatusBarStyle = .default { didSet { setNeedsStatusBarAppearanceUpdate() } } override var preferredStatusBarStyle: UIStatusBarStyle { return statusBarStyle } private var interactionController: UIPercentDrivenInteractiveTransition? override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white let cardView = UIView(frame: .zero) cardView.translatesAutoresizingMaskIntoConstraints = false cardView.backgroundColor = UIColor(red:0.976, green:0.976, blue:0.976, alpha:1) view.addSubview(cardView) let borderView = UIView(frame: .zero) borderView.translatesAutoresizingMaskIntoConstraints = false borderView.backgroundColor = UIColor(red:0.697, green:0.698, blue:0.697, alpha:1) view.addSubview(borderView) let cardViewTextLabel = UILabel(frame: .zero) cardViewTextLabel.translatesAutoresizingMaskIntoConstraints = false cardViewTextLabel.text = "Tap or drag" cardViewTextLabel.font = UIFont.boldSystemFont(ofSize: 16) view.addSubview(cardViewTextLabel) let cardViewConstraints = [ cardView.heightAnchor.constraint(equalToConstant: 44), cardView.leadingAnchor.constraint(equalTo: view.leadingAnchor), cardView.trailingAnchor.constraint(equalTo: view.trailingAnchor), cardView.bottomAnchor.constraint(equalTo: view.bottomAnchor), borderView.heightAnchor.constraint(equalToConstant: 0.5), borderView.topAnchor.constraint(equalTo: cardView.topAnchor), borderView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor), borderView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor), cardViewTextLabel.centerXAnchor.constraint(equalTo: cardView.centerXAnchor), cardViewTextLabel.centerYAnchor.constraint(equalTo: cardView.centerYAnchor) ] NSLayoutConstraint.activate(cardViewConstraints) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handlePresentTapGesture(gestureRecognizer:))) cardView.addGestureRecognizer(tapGestureRecognizer) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePresentPanGesture(gestureRecognizer:))) cardView.addGestureRecognizer(panGestureRecognizer) } // MARK: Actions @objc private func handlePresentTapGesture(gestureRecognizer: UITapGestureRecognizer) { let viewController = createViewController() present(viewController, animated: true, completion: nil) } @objc private func handlePresentPanGesture(gestureRecognizer: UIPanGestureRecognizer) { let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview) let height = (gestureRecognizer.view?.superview?.bounds.height)! - 40 let percentage = abs(translation.y / height) switch gestureRecognizer.state { case .began: interactionController = UIPercentDrivenInteractiveTransition() let viewController = createViewController() present(viewController, animated: true, completion: nil) case .changed: interactionController?.update(percentage) case .ended: if percentage < 0.5 { interactionController?.cancel() } else { interactionController?.finish() } interactionController = nil default: break } } @objc private func handleDismissTapGesture(gestureRecognizer: UITapGestureRecognizer) { dismiss(animated: true, completion: nil) } @objc private func handleDismissPanGesture(gestureRecognizer: UIPanGestureRecognizer) { let translation = gestureRecognizer.translation(in: gestureRecognizer.view) let height = (gestureRecognizer.view?.bounds.height)! let percentage = (translation.y / height) switch gestureRecognizer.state { case .began: interactionController = UIPercentDrivenInteractiveTransition() dismiss(animated: true, completion: nil) case .changed: interactionController?.update(percentage) case .ended: if percentage < 0.5 { interactionController?.cancel() } else { interactionController?.finish() } interactionController = nil default: break } } // MARK: UIViewControllerTransitioningDelegate func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { return PresentationController(presentedViewController: presented, presenting: presenting) } func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { // Get UIKit to animate if it's not an interative animation return interactionController != nil ? AnimationController(direction: .present) : nil } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { // Get UIKit to animate if it's not an interative animation return interactionController != nil ? AnimationController(direction: .dismiss) : nil } func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return interactionController } func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return interactionController } // MARK: Private func createViewController() -> UIViewController { let viewController = UIViewController(nibName: nil, bundle: nil) viewController.title = "Tap or drag" viewController.view.backgroundColor = .white let navigationController = UINavigationController(rootViewController: viewController) navigationController.transitioningDelegate = self navigationController.modalPresentationStyle = .custom UINavigationBar.appearance().titleTextAttributes = [NSFontAttributeName: UIFont.boldSystemFont(ofSize: 16)] let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDismissTapGesture(gestureRecognizer:))) navigationController.view.addGestureRecognizer(tapGestureRecognizer) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleDismissPanGesture(gestureRecognizer:))) navigationController.view.addGestureRecognizer(panGestureRecognizer) return navigationController } } 

好吧,我会尽量给你一个紧凑的解决scheme,用最less的代码。

快速解决scheme 您需要以modalPresentationStyle – 属性设置为.overCurrentContext呈现控制器。 您可以在preset(controller:...) – 方法被调用之前或在prepare(for:...) – 如果它是一个继续转换之前设置值。 向上滑动使用modalTransitionStyle设置为.coverVertical

要“缩小”源视图,只需在viewWill(Diss)appear更新其边界就会viewWill(Diss)appear方法。 在大多数情况下,这将工作。

不要忘记将模态控制器背景视图设置为透明,以便底层视图仍然可见。

顺利地上下滑动。 您需要以适当的方式在控制器之间build立一个转换 。 如果你仔细观察苹果音乐应用程序,你会看到一种方法来隐藏顶部控制器的滑下手势。 您也可以自定义您的视图(dis)外观。 看看这篇文章 。 它只使用UIKit方法。 不幸的是,这种方式需要大量代码,但是您可以使用第三方库来设置转换。 像这样