iOS 10中的音乐应用程序采用了类似卡片的新外观:正在播放屏幕向上滑动,而层次结构中的下方视图缩小,在屏幕顶部略微突出。
以下是Mail compose窗口中的示例:
这个比喻也可以在流行的播客播放器Overcast中看到:
UIKit中是否有功能可以实现这种类似卡片的外观?
您可以在界面构建器中构建segue。选择从ViewController
到CardViewController
的模态segue。
对于你的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
}
}
}
最后,您还需要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)
}
}
和:
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)
})
}
}
最后,为了动画CardViewController
关闭,勾选你的关闭按钮到FirstResponder
选择dismiss
并将此方法添加到ViewController
:
func dismiss(_ segue: UIStoryboardSegue) {
self.dismiss(animated: true, completion: nil)
}
Apple在UIViewPropertyAnimator
中展示如何使用WWDC 2017 Session 230: Advanced Animations with UIKit执行此操作
我在这里有一个这种技术的演示:https://github.com/peteog/CardUI
基本思想是添加子视图控制器,并将其大部分放在屏幕外。点击/平移时,您可以为子视图控制器的框架设置动画。
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()
}
}
好的,我会尝试用最少的代码为您提供紧凑的解决方案。
快速解决方案您需要以modalPresentationStyle
-property设置为.overCurrentContext
来模态地呈现控制器。您可以在调用preset(controller:...)
-method之前设置值,或者在prepare(for:...)
-one中设置值,如果它是segue过渡。为了向上滑动使用modalTransitionStyle
设置为.coverVertical
。
要“缩小”源视图,只需在viewWill(Diss)appear
-methods中更新其边界即可。在大多数情况下,这将起作用。
不要忘记将模态控制器背景视图设置为透明,以便底层视图仍然可见。
顺畅地上下滑动。您需要以适当的方式在控制器之间设置transition。如果您仔细观察Apple音乐应用程序,您将看到一种通过向下滑动手势隐藏顶部控制器的方法。您也可以自定义视图(dis)外观。看看this article。它仅使用UIKit
方法。不幸的是,这种方式需要大量代码,但您可以使用第三方库来设置转换。像this one。
如果您乐意添加第三方依赖项,那么请尝试使用SPStorkController,它是最新的(Swift 4.2在撰写本文时)并且使用最少的配置。