普通のUINavigation
のpush
の遷移はわかりやすいしアニメーションも最低限実装されているが、少し単調というか見飽きたというか。色々とカスタマイズしていきたいなと思いました。
純正アプリであるAppStoreアプリですが、カードが一覧で表示されてタップすると詳細画面にアニメーションで遷移する、このアニメーションめっちゃかっこいいですよね、是非真似していきたい、実装してみたい、ということで実装してみました。
実際には少し違うものが出来上がったのですが、ほとんど同じような挙動です。
AppStoreなど標準のiOSアプリは既にSwiftUI
で置き換えられているのかもしれないですが、まだまだ知識経験がdeveloper全体の中でも自分としてもまだ十分ではないと感じています。UIKit
では簡単に表現できていたことがSwiftUI
ではまだまだ出来ないという部分も多くあると思います。まだまだ実際のプロダクトではUIKit
をガッツリと使っていく状態は続くと思うので(趣味のアプリは別として)、今回もUIKit
でAppStoreアプリと同じトランジションアニメーションを実装してみました。
少し複雑にはなるので、トランジションのアニメーション自体が初めての場合は、以下の記事のほうがより易しいです。
🍮
標準アプリAppStoreの確認(目標の確認)
まずは目標とするアプリの挙動を確認してみます。
このように、カードが一覧で表示されていて、タップすると、カードが広がりつつ詳細画面に遷移する、詳細画面からの遷移ではカードが縮小していくようなアニメーションがあり、またUIScrollView
のcontentOffsetY
が0
の状態で下スクロールすると、スクロールの大きさによってカード自体が小さく縮小していくアニメーション、そしてスクロールが一定量超えるとdismiss
されるようなアニメーションです。
実装したアプリの挙動確認
以下の動画は実際に今回作成したアプリの挙動です。
AppStoreとほとんど同じですが、カードはシンプルにしています。画像をカードいっぱいに表示しており、その上にUILabel
を載せているのみです。タップすると同じようなアニメーションで拡大しつつ詳細画面に遷移させています。
サンプルアプリの構成要素
では実際に実装の解説に入っていきます。まずは必要となる構成要素からです。
- 一覧画面(ListViewController)
- 詳細画面(DetailViewController)
- カードビュー(CardView)
- カードビューセル(CardViewCell)
- カードビューモデル(CardViewModel)
- カードトランジションマネージャー(CardTransitionManager)
今回の実装で重要なのは上記の6つです。
一覧画面(ListViewController)
まずは一覧画面です。こちらはUITableView
をListViewController
のview
にadd
し、このtableView
にCardViewCell
を表示しています。サンプルなのでセルの数は3つと決め打ちしています。
import UIKit
import SnapKit
class ListViewController: UIViewController {
let transitionManager = CardTransitionManager()
let cardViewModels: [CardViewModel] = [
CardViewModel(image: UIImage(named: "card1")!, title: "FIRST CARD TITLE LABEL", subtitle: "This is a subtitle label."),
CardViewModel(image: UIImage(named: "card2")!, title: "SECOND CARD TITLE LABEL", subtitle: "This is a subtitle label."),
CardViewModel(image: UIImage(named: "card3")!, title: "THIRD CARD TITLE LABEL", subtitle: "This is a subtitle label."),
]
lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .none
tableView.register(CardViewCell.self, forCellReuseIdentifier: CardViewCell.identifier)
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
}
extension ListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = DetailViewController(cardViewModel: cardViewModels[indexPath.row])
vc.modalPresentationStyle = .fullScreen
vc.transitioningDelegate = transitionManager
present(vc, animated: true, completion: nil)
}
}
extension ListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cardViewModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CardViewCell.identifier, for: indexPath) as! CardViewCell
cell.configureCell(cardViewModel: cardViewModels[indexPath.row])
return cell
}
func selectedCellCardView() -> CardView? {
guard let indexPath = tableView.indexPathForSelectedRow else { return nil }
let cell = tableView.cellForRow(at: indexPath) as! CardViewCell
return cell.cardView
}
}
extension ListViewController {
var selectedCell: UITableViewCell? {
guard let indexPath = tableView.indexPathForSelectedRow else { return nil }
return tableView.cellForRow(at: indexPath)
}
var selectedCellCardView: CardView? {
let cell = selectedCell as? CardViewCell
return cell?.cardView
}
}
AutoLayout
特にポイントというほどのポイントもないのですが、今回もSnapKit
を使っています。SnapKit
は、コードでAutoLayout
を少ない記述量で実装できるライブラリです。もしSnapKitを使わず純粋なコードで実装する場合は、 tableView.translatesAutoresizingMaskIntoConstraints = false
が必要になります。
CardViewModel
UITableView
に表示するセルですが、上記のように、決め打ちで3つとしています。
先にCardViewModel
のコードを記載します。
import UIKit
struct CardViewModel {
let image: UIImage
let title: String
let subtitle: String
}
これだけです。必要最低限だけを実装しています。
image
は、カードの背景に目一杯表示する画像です。Assets.scassets
に画像を3つ格納し、それを読み込んでいるだけです。
title
は、カードの真ん中に大きく表示するUILable
に使用しています。
subtitle
は、カードの左下に小さく表示するUILabel
に使用しています。正直なくても問題ありません。
CardViewModelのイニシャライズ
CardViewModel
の定義は上記のみで、ListViewController
ではこのCardViewModel
をイニシャライズしています。
CardViewModel
には、自動的にメンバーワイズイニシャライザーが実装されます。CardViewModel(
と打ち込むとコード補完でイニシャライズ用コードが表示されますので、補完候補をそのまま使ってしまいます。
選択されているCell及びそのCardViewの取得
selectedCell
とselectedCellCardView
は選択されているセルとそのcardView
を取得するために実装したcomputed property
です。これは後にトランジションアニメーションを実装するときに使用します。
詳細画面(DetailViewController)
次は遷移先の詳細画面です。
本記事のメインはアニメーション部分ですが、この詳細画面でもいくつかのポイントに触れておきます。
まず元のアプリであるAppStoreアプリの挙動を確認してみてください。
詳細画面では、まずスクロールが可能で、詳細画面で開いたアプリなどの詳細な情報が確認できるようになっています。ここでは、詳細説明の文章や画像などが表示されていますが、今回実装するアプリでは文字列(UILabel)のみを表示するようにします。
詳細画面の構成要素
立体的にみると次のような実装になっています。
- UIScrollView
- CardView
- UILabel
- UIButton
主な構成要素はこの4点です。
DetailViewController
のview
の上にはUIScrollView
、UIScrollView
の上部には今回独自で実装するCardView
と閉じる用のUIButton
、そしてCardView
の下側には、UILabel
という構成になっています。
DetailViewControllerのイニシャライズ
詳細画面では、独自クラスの1つのインスタンスを詳細に表示するという目的の画面なので、独自クラスの1つのインスタンスでDetailViewController
をイニシャライズするように実装しています。
let cardViewModel: CardViewModel
var cardView: CardView
init(cardViewModel: CardViewModel) {
self.cardViewModel = cardViewModel
self.cardView = CardView(cardViewModel: cardViewModel)
super.init(nibName: nil, bundle: nil)
}
snapshotView
次に、スクロールでdismiss
するときのアニメーションのため、snapshotView
を実装しておきます。
private lazy var snapshotView: UIImageView = {
let imageView = UIImageView()
imageView.backgroundColor = .white
imageView.layer.shadowColor = UIColor.black.cgColor
imageView.layer.shadowOpacity = 0.2
imageView.layer.shadowRadius = 10.0
imageView.layer.shadowOffset = CGSize(width: -1, height: 2)
imageView.isHidden = true
return imageView
}()
AppStoreアプリの挙動とは少し違う部分がありますが、概ね同じです。UIScrollView
のスクロール部分が一番上にある状態でさらに下側にスワイプすると、DetailViewController
自体が小さく縮小し、スクロール量が閾値を超えると、dismiss
を発火するようにします。
このときに縮小するUIはDetailController
のview
そのものではなく、そのview
のUIをUIImage
としてコピーし、その画像を縮小していく、という方法をとっています。
import UIKit
extension UIView {
func createSnapshot() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(frame.size, false, 0.0)
drawHierarchy(in: frame, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
役割分担のため、スナップショットのUIImageを作成するためのextensionを実装しておきます。
func createSnapshotOfView() {
let snapshotImage = view.createSnapshot()
snapshotView.image = snapshotImage
scrollView.addSubview(snapshotView)
snapshotView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
}
あとは、関数を定義しておき、トランジションの中すぐに呼び出せるようにしておきます。
UIScrollViewの実装
スクロールの話も出たのでここで記載しておきます。
上述の通り、スクロールでdismiss
出来るように実装していきます。
このとき、isTracking
によって、scrollView.bounces
を操作したり、スクロール量によって、諸々のviewの表示/非表示切り替え、snapshotView
の表示/非表示、縮小などを実装していきます。
そしてスクロール量が閾値を超えると、dismiss
します。
if yPositionForDismissal + yContentOffset <= 0 {
dismiss(animated: true, completion: nil)
}
DetailViewControllerのコード
import UIKit
class DetailViewController: UIViewController {
var viewsAreHidden: Bool = false {
didSet {
cardView.isHidden = viewsAreHidden
descriptionLabel.isHidden = viewsAreHidden
closeButton.isHidden = viewsAreHidden
}
}
override var prefersStatusBarHidden: Bool {
return true
}
fileprivate lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.showsHorizontalScrollIndicator = false
view.showsVerticalScrollIndicator = false
view.bounces = true
view.clipsToBounds = true
view.contentInsetAdjustmentBehavior = .never
view.delegate = self
return view
}()
lazy var closeButton: UIButton = {
let button = UIButton()
let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .black, scale: .large)
let image = UIImage(systemName: "xmark.circle.fill", withConfiguration: config)!
button.setImage(image, for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(closeButtonTapped(_:)), for: .touchUpInside)
return button
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textColor = UIColor.black.withAlphaComponent(0.8)
let style = NSMutableParagraphStyle()
style.lineSpacing = 10
style.alignment = .left
let attributes: [NSAttributedString.Key : Any] = [
.font : UIFont.systemFont(ofSize: 16, weight: .semibold),
.paragraphStyle: style,
]
label.attributedText = NSAttributedString(string: String.lorem200, attributes: attributes)
return label
}()
private lazy var snapshotView: UIImageView = {
let imageView = UIImageView()
imageView.backgroundColor = .white
imageView.layer.shadowColor = UIColor.black.cgColor
imageView.layer.shadowOpacity = 0.2
imageView.layer.shadowRadius = 10.0
imageView.layer.shadowOffset = CGSize(width: -1, height: 2)
imageView.isHidden = true
return imageView
}()
let cardViewModel: CardViewModel
var cardView: CardView
init(cardViewModel: CardViewModel) {
self.cardViewModel = cardViewModel
self.cardView = CardView(cardViewModel: cardViewModel)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
view.addSubview(scrollView)
scrollView.addSubview(cardView)
scrollView.addSubview(descriptionLabel)
view.addSubview(closeButton)
scrollView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
cardView.snp.makeConstraints {
$0.top.leading.trailing.equalToSuperview()
$0.width.equalTo(UIScreen.main.bounds.width)
}
closeButton.snp.makeConstraints {
$0.top.equalToSuperview().offset(16)
$0.trailing.equalToSuperview().offset(-16)
}
descriptionLabel.snp.makeConstraints {
$0.top.equalTo(cardView.snp.bottom).offset(16)
$0.leading.equalToSuperview().offset(16)
$0.trailing.bottom.equalToSuperview().offset(-16)
}
}
}
extension DetailViewController {
@objc func closeButtonTapped(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
func createSnapshotOfView() {
let snapshotImage = view.createSnapshot()
snapshotView.image = snapshotImage
scrollView.addSubview(snapshotView)
snapshotView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
}
}
extension DetailViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yPositionForDismissal: CGFloat = 30.0
let yContentOffset = scrollView.contentOffset.y
updateCloseButton(yContentOffset: yContentOffset)
if scrollView.isTracking {
scrollView.bounces = true
} else {
scrollView.bounces = yContentOffset > 0
}
if yContentOffset < 0 && scrollView.isTracking {
viewsAreHidden = true
snapshotView.isHidden = false
let scale = (100 + (yContentOffset/2.5)) / 100
snapshotView.transform = CGAffineTransform(scaleX: scale, y: scale)
snapshotView.layer.cornerRadius = -yContentOffset > yPositionForDismissal ? yPositionForDismissal : -yContentOffset
if yPositionForDismissal + yContentOffset <= 0 {
dismiss(animated: true, completion: nil)
}
} else {
viewsAreHidden = false
snapshotView.isHidden = true
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
scrollView.bounces = true
}
func updateCloseButton(yContentOffset: CGFloat) {
if yContentOffset < 320 {
closeButton.tintColor = .white
} else {
closeButton.tintColor = .darkGray
}
}
}
CardView / CardViewCell
CardViewの実装
一覧画面で表示するCardView
を実装していきます。
import UIKit
import SnapKit
final class CardView: UIView {
lazy var backgroundImageView: UIImageView = {
let view = UIImageView()
view.layer.masksToBounds = true
return view
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 40, weight: .bold)
label.textColor = .white
label.numberOfLines = 0
return label
}()
lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = .white
label.numberOfLines = 0
return label
}()
let cardViewModel: CardViewModel
init(cardViewModel: CardViewModel) {
self.cardViewModel = cardViewModel
super.init(frame: .zero)
addSubview(backgroundImageView)
backgroundImageView.addSubview(titleLabel)
backgroundImageView.addSubview(subtitleLabel)
backgroundImageView.image = cardViewModel.image
titleLabel.text = cardViewModel.title
subtitleLabel.text = cardViewModel.subtitle
backgroundImageView.snp.remakeConstraints {
$0.top.leading.equalToSuperview()
$0.trailing.bottom.equalToSuperview()
$0.height.equalTo(332)
}
titleLabel.snp.remakeConstraints {
$0.centerY.equalToSuperview()
$0.leading.equalToSuperview().offset(16)
$0.trailing.equalToSuperview().offset(-16)
}
subtitleLabel.snp.remakeConstraints {
$0.leading.equalToSuperview().offset(16)
$0.bottom.equalToSuperview().offset(-16)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
特に注意点などはないですが、こちらもCardViewModel
でイニシャライズするように実装しています。
CardViewCellの実装
import UIKit
import SnapKit
class CardViewCell: UITableViewCell {
static let identifier: String = {
return String(describing: self)
}()
lazy var shadowView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 20
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.3
view.layer.shadowRadius = 8
view.layer.shadowOffset = .zero
return view
}()
var cardView: CardView?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
}
func configureCell(cardViewModel: CardViewModel) {
contentView.subviews.forEach {
$0.removeFromSuperview()
}
cardView = CardView(cardViewModel: cardViewModel)
guard let cardView = cardView else { return }
cardView.layer.cornerRadius = 20
cardView.clipsToBounds = true
contentView.addSubview(shadowView)
contentView.addSubview(cardView)
cardView.snp.makeConstraints {
$0.top.leading.equalToSuperview().offset(16)
$0.trailing.bottom.equalToSuperview().offset(-16)
}
shadowView.snp.makeConstraints {
$0.edges.equalTo(cardView)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
CardView
自体をセルの上にadd
しており、cornerRadius
及び上下左右のスペースをAutoLayout
で追加しています。
また、カード感を表現するために、CardView
の裏側にshadowView
もadd
しています。
これで、CardView
をCardViewCell
で表示するときには、影と角丸が追加され、詳細画面ではCardView
のみなので角丸と上下左右のスペースはなく、画面いっぱいに表示されるような実装になります。
CardTransitionManagerの実装
前回の記事と色々説明が重複するところがあるので、ポイントだけ記載します。
🍮
アニメーションのポイント
AppStoreアプリのアニメーションを確認して実装の方向性を検討します。今回実装することにしたポイントは以下の通りです。
- 一覧から詳細にアニメーションする時、カードが少し小さくアニメーションしてから詳細画面に向けて拡大する。
- 詳細から一覧にアニメーションするときは、すなおに、元の大きさにアニメーションするのみ。
- 一覧から詳細にアニメーションするとき、裏側に白いViewがアニメーションで現れ、カードが広がっていくアニメーションを実装する。その際、裏側に表示されている一覧画面は徐々にblur効果がかかってぼやけるように見える。
- 一覧から詳細、詳細から一覧の両方向のアニメーションにおいて、少しバウンドするようなアニメーションになっている。
import UIKit
enum CardTransitionType {
case presentation
case dismissal
var blurAlpha: CGFloat { return self == .presentation ? 1 : 0 }
var dimAlpha: CGFloat { return self == .presentation ? 0.5 : 0 }
var closeAlpha: CGFloat { return self == .presentation ? 1 : 0 }
var cornerRadius: CGFloat { return self == .presentation ? 20.0 : 0 }
var next: CardTransitionType { return self == .presentation ? .dismissal : .presentation }
}
class CardTransitionManager: NSObject {
let transitionDuration: Double = 0.8
var transition: CardTransitionType = .presentation
let shrinkDuration: Double = 0.2
lazy var blurEffectView: UIVisualEffectView = {
let blurEffect = UIBlurEffect(style: .light)
let visualEffectView = UIVisualEffectView(effect: blurEffect)
return visualEffectView
}()
lazy var dimmingView: UIView = {
let view = UIView()
view.backgroundColor = .black
return view
}()
lazy var whiteView: UIView = {
let view = UIView()
view.backgroundColor = .white
return view
}()
lazy var closeButton: UIButton = {
let button = UIButton()
let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .black, scale: .large)
let image = UIImage(systemName: "xmark.circle.fill", withConfiguration: config)!
button.setImage(image, for: .normal)
button.tintColor = .white
return button
}()
private func addBackgroundView(to containerView: UIView) {
blurEffectView.frame = containerView.frame
blurEffectView.alpha = transition.next.blurAlpha
containerView.addSubview(blurEffectView)
dimmingView.frame = containerView.frame
dimmingView.alpha = transition.next.dimAlpha
containerView.addSubview(dimmingView)
}
private func createCardViewCopy(cardView: CardView) -> CardView {
let cardViewCopy = CardView(cardViewModel: cardView.cardViewModel)
cardViewCopy.frame = cardView.frame
return cardViewCopy
}
}
extension CardTransitionManager: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return transitionDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
switch transition {
case .presentation:
presentTransition(using: transitionContext)
case .dismissal:
dismissalTransition(using: transitionContext)
}
}
private func presentTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
containerView.subviews.forEach { $0.removeFromSuperview() }
guard let fromView = transitionContext.viewController(forKey: .from) as? ListViewController,
let toView = transitionContext.viewController(forKey: .to) as? DetailViewController
else { return }
guard let cardView = fromView.selectedCellCardView else { return }
let cardViewCopy = createCardViewCopy(cardView: cardView)
cardViewCopy.frame = containerView.convert(cardView.frame, from: cardView.superview)
cardViewCopy.layoutIfNeeded()
toView.view.frame = transitionContext.finalFrame(for: toView)
toView.view.alpha = 0
toView.view.layoutIfNeeded()
containerView.addSubview(fromView.view)
addBackgroundView(to: containerView)
whiteView.frame = cardViewCopy.frame
whiteView.layer.cornerRadius = transition.cornerRadius
containerView.addSubview(whiteView)
containerView.addSubview(cardViewCopy)
containerView.addSubview(toView.view)
cardViewCopy.addSubview(closeButton)
closeButton.snp.makeConstraints {
$0.top.equalToSuperview().offset(16)
$0.trailing.equalToSuperview().offset(-16)
}
closeButton.alpha = transition.next.closeAlpha
cardViewCopy.backgroundImageView.layer.cornerRadius = transition.cornerRadius
fromView.selectedCell?.isHidden = true
cardView.isHidden = true
moveAndConvertToCardView(cardView: cardViewCopy, containerView: containerView, targetFrame: toView.cardView.frame) {
toView.view.alpha = 1
fromView.view.removeFromSuperview()
cardViewCopy.removeFromSuperview()
fromView.selectedCell?.isHidden = false
cardView.isHidden = false
toView.createSnapshotOfView()
transitionContext.completeTransition(true)
}
}
private func dismissalTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
containerView.subviews.forEach { $0.removeFromSuperview() }
guard let fromView = transitionContext.viewController(forKey: .from) as? DetailViewController,
let toView = transitionContext.viewController(forKey: .to) as? ListViewController
else { return }
let cardView = fromView.cardView
let cardViewCopy = createCardViewCopy(cardView: cardView)
let targetCardView = toView.selectedCellCardView!
let targetCardViewFrame = containerView.convert(targetCardView.frame, from: targetCardView.superview)
cardViewCopy.frame = containerView.convert(cardView.frame, from: cardView.superview)
cardViewCopy.layoutIfNeeded()
toView.view.frame = transitionContext.finalFrame(for: toView)
toView.view.layoutIfNeeded()
containerView.addSubview(toView.view)
addBackgroundView(to: containerView)
whiteView.frame = containerView.frame
whiteView.layer.cornerRadius = transition.cornerRadius
containerView.addSubview(whiteView)
containerView.addSubview(cardViewCopy)
cardViewCopy.addSubview(closeButton)
closeButton.snp.makeConstraints {
$0.top.equalToSuperview().offset(16)
$0.trailing.equalToSuperview().offset(-16)
}
closeButton.alpha = transition.next.closeAlpha
cardViewCopy.backgroundImageView.layer.cornerRadius = transition.cornerRadius
targetCardView.isHidden = true
toView.selectedCell?.isHidden = true
cardView.isHidden = true
moveAndConvertToCardView(cardView: cardViewCopy, containerView: containerView, targetFrame: targetCardViewFrame) {
cardViewCopy.removeFromSuperview()
toView.selectedCell?.isHidden = false
targetCardView.isHidden = false
cardView.isHidden = false
transitionContext.completeTransition(true)
}
}
func makeShrinkAnimator(for cardView: CardView) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: shrinkDuration, curve: .easeOut) {
cardView.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
self.whiteView.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
self.dimmingView.alpha = 0.05
}
}
func makeExpandContractAnimator(for cardView: CardView, in containerView: UIView, to targetFrame: CGRect) -> UIViewPropertyAnimator {
let springTiming = UISpringTimingParameters(dampingRatio: 0.75, initialVelocity: CGVector(dx: 0, dy: 2))
let animator = UIViewPropertyAnimator(duration: transitionDuration - shrinkDuration, timingParameters: springTiming)
animator.addAnimations {
cardView.transform = .identity
cardView.frame = targetFrame
cardView.backgroundImageView.layer.cornerRadius = self.transition.next.cornerRadius
self.blurEffectView.alpha = self.transition.blurAlpha
self.dimmingView.alpha = self.transition.dimAlpha
self.closeButton.alpha = self.transition.closeAlpha
containerView.layoutIfNeeded()
self.whiteView.frame = self.transition == .presentation ? containerView.frame : cardView.frame
self.whiteView.layer.cornerRadius = self.transition.next.cornerRadius
self.whiteView.transform = .identity
}
return animator
}
func moveAndConvertToCardView(cardView: CardView, containerView: UIView, targetFrame: CGRect, completion: @escaping () -> ()) {
let shrinkAnimator = makeShrinkAnimator(for: cardView)
let expandContractAnimator = makeExpandContractAnimator(for: cardView, in: containerView, to: targetFrame)
expandContractAnimator.addCompletion { _ in
completion()
}
switch transition {
case .presentation:
shrinkAnimator.addCompletion { _ in
cardView.layoutIfNeeded()
expandContractAnimator.startAnimation()
}
shrinkAnimator.startAnimation()
case .dismissal:
expandContractAnimator.startAnimation()
}
}
}
extension CardTransitionManager: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition = .presentation
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition = .dismissal
return self
}
}
一番大事な詳細説明で、力尽きました。。