Подтвердить что ты не робот

Есть ли открытый API для интерфейса просмотра карт, который можно увидеть в iOS 10?

Музыкальное приложение в iOS 10 принимает новый вид, похожий на карточку: теперь экран "Воспроизведение" переливается вверх, в то время как вид ниже в иерархии масштабируется, слегка выступая в верхней части экрана.

music app card interface

Вот пример из окна Mail compose:

mail compose card interface

Эта метафора также может быть замечена в Overcast, популярном подкасте:

overcast card interface

Есть ли функция в UIKit для достижения такого карточного вида?

4b9b3361

Ответ 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 -методы. К сожалению, этот способ требует большого количества кода, но вы можете использовать сторонние библиотеки для настройки переходов. Как этот.