framework - ¿Existe una API pública para la interfaz de usuario de visualización de tarjeta que se puede ver en iOS 10?
que es uikit ios (3)
Ok, intentaré darle una solución compacta con un mínimo de código.
Solución rápida. modalPresentationStyle
presentar un controlador de manera modal con modalPresentationStyle
- property establecida en .overCurrentContext
. Puede establecer el valor antes de preset(controller:...)
-method get call o en prepare(for:...)
-one si se trata de una transición segue. Para deslizar hacia arriba use modalTransitionStyle
establecido en .coverVertical
.
Para "alejar" la vista de origen, simplemente actualice sus límites en la viewWill(Diss)appear
métodos viewWill(Diss)appear
. En la mayoría de los casos esto funcionará.
No olvide configurar la vista de fondo de su controlador modal de forma transparente para que la vista subyacente siga siendo visible.
Deslizándose hacia arriba / abajo suavemente. Es necesario configurar una transition entre los controladores de una manera adecuada. Si te fijas más en la aplicación de música de Apple, verás una forma de ocultar el controlador superior con un gesto deslizante hacia abajo. Puede personalizar su vista (des) apariencia también. Echa un vistazo a este artículo . Utiliza UIKit
métodos UIKit
. Desafortunadamente, esta forma requiere mucho código, pero puede usar bibliotecas de terceros para configurar las transiciones. Como este
La aplicación de música en iOS 10 adopta una nueva apariencia de tarjeta: la pantalla Reproduciendo ahora se desliza hacia arriba, mientras que la vista de abajo en la jerarquía se aleja, sobresaliendo ligeramente en la parte superior de la pantalla.
Aquí está el ejemplo de la ventana de redacción de Mail:
Esta metáfora también se puede ver en Overcast, el popular reproductor de podcast:
¿Hay una función en UIKit para lograr esta apariencia de tarjeta?
Actualización: la parte interactiva de esta demostración no funciona en iOS 11 por alguna razón. Apple muestra una técnica diferente en WWDC 2017 Sesión 230: Animaciones avanzadas con UIKit donde utilizan UIViewPropertyAnimator
Tengo una demostración básica de esta técnica aquí: https://github.com/peteog/CardUI
Este tipo de interfaz de usuario se puede crear utilizando transiciones UIViewController personalizadas , UIPresentationController
y UIViewPropertyAnimator
.
Aplicación de ejemplo: https://github.com/peteog/CardUIExample
Primero crea una subclase UIPresentationController
. Esta voluntad:
- Añadir una vista de atenuación
- Transforme el controlador de vista de presentación para insertarlo desde la barra de estado
- Establece el marco de la vista presentada para hacer el efecto de la carta.
Código:
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
})
}
}
A continuación necesitamos un objeto que hará la animación entre las dos pantallas:
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
}
}
Finalmente, enganche todo junto en el controlador de vista, agregando reconocedores de gestos para controlar la transición interactiva.
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
}
}
Puedes construir el segue en el constructor de interfaces. Seleccionando el modo modal de ViewController
a CardViewController
.
Para su 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
}
}
a continuación, agregue esta extensión:
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
}
}
}
Finalmente, necesitarás 2 clases más:
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)
}
}
y:
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)
})
}
}
Finalmente, para animar el cierre de CardViewController
, enganche su botón de cierre a FirstResponder
seleccione dismiss
y agregue este método a ViewController
:
func dismiss(_ segue: UIStoryboardSegue) {
self.dismiss(animated: true, completion: nil)
}