Есть ли открытый API для интерфейса просмотра карт, который можно увидеть в iOS 10?
Музыкальное приложение в iOS 10 принимает новый вид, похожий на карточку: теперь экран "Воспроизведение" переливается вверх, в то время как вид ниже в иерархии масштабируется, слегка выступая в верхней части экрана.
![music app card interface]()
Вот пример из окна Mail compose:
![mail compose card interface]()
Эта метафора также может быть замечена в Overcast, популярном подкасте:
![overcast card interface]()
Есть ли функция в UIKit для достижения такого карточного вида?
Ответы
Ответ 1
Вы можете построить segue в построителе интерфейса. Выбор модального сегмента от ViewController
до 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
}
}
}
Наконец, вам понадобится еще 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 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 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)
}
Ответ 2
Обновление: интерактивная часть этой демонстрации не работает на iOS 11 по какой-то причине 😞. Apple показывает другую методику в WWDC 2017 Сессия 230: Продвинутые анимации с UIKit, где они используют UIViewPropertyAnimator
У меня есть базовая демонстрация этого метода здесь: https://github.com/peteog/CardUI
Такой пользовательский интерфейс может быть создан с использованием пользовательских переходов UIPresentationController
, UIViewPropertyAnimator
и UIViewPropertyAnimator
.
Пример приложения: https://github.com/peteog/CardUIExample
![iUbIG.gif]()
Сначала создайте подкласс 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
})
}
}
Затем нам нужен объект, который будет делать анимацию между двумя экранами:
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 not an interative animation
return interactionController != nil ? AnimationController(direction: .present) : nil
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// Get UIKit to animate if it 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
}
}
Ответ 3
Хорошо, я постараюсь дать вам компактное решение с минимальным кодом.
Быстрое решение.
Вам необходимо представить контроллер с помощью modalPresentationStyle
- свойство, установленного на .overCurrentContext
. Вы можете установить значение до вызова preset(controller:...)
-method или в prepare(for:...)
-one, если это переход segue. Для разворачивания используйте modalTransitionStyle
для .coverVertical
.
Чтобы просмотреть "исходный вид", просто обновите его границы в viewWill(Diss)appear
-методах. В большинстве случаев это будет работать.
Не забудьте установить прозрачный вид фона модального контроллера, чтобы основной вид все еще был видимым.
Сглаживание вверх/вниз плавно. Вам нужно настроить transition между контроллерами в правильном путь. Если вы посмотрите на приложение Apple music, вы увидите способ скрыть верхний контроллер с жестом слайда. Вы также можете настроить внешний вид (dis). Взгляните на в этой статье. Он использует только UIKit
-методы. К сожалению, этот способ требует большого количества кода, но вы можете использовать сторонние библиотеки для настройки переходов. Как этот.