Created
April 29, 2023 10:28
-
-
Save fumiyasac/549654724c711e96f9e1fa989931acf1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ========== | |
// 5/10登壇補足資料 | |
// 👉 UICollectionView内のサムネイル画像が浮かび上がる様なCustomTransitionをPush/Popの画面遷移に実装するコード例 | |
// ========== | |
// 1. DetailTransitionクラス実装例 | |
import Foundation | |
import UIKit | |
class DetailTransition: NSObject { | |
// アニメーション対象となる画像のtag番号(遷移先のUIImageViewに付与する) | |
private let customAnimatorTag = 99 | |
// トランジションの秒数 | |
private let duration: TimeInterval = 0.28 | |
// トランジションの方向(push: true, pop: false) | |
var presenting: Bool = true | |
// アニメーション対象なるViewControllerの位置やサイズ情報を格納するメンバ変数 | |
var originFrame: CGRect = CGRect.zero | |
// アニメーション対象なるサムネイル画像情報を格納するメンバ変数 | |
var originImage: UIImage = UIImage() | |
} | |
// MARK: - UIViewControllerAnimatedTransitioning | |
extension DetailTransition: UIViewControllerAnimatedTransitioning { | |
// アニメーションの時間を定義する | |
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { | |
return duration | |
} | |
// アニメーションの実装を定義する | |
// 画面遷移コンテキスト(UIViewControllerContextTransitioning)を利用する | |
// → 遷移元や遷移先のViewControllerやそのほか関連する情報が格納されているもの | |
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
// コンテキストを元にViewのインスタンスを取得する(存在しない場合は処理を終了) | |
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else { | |
return | |
} | |
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { | |
return | |
} | |
// アニメーションの実体となるContainerViewを作成する | |
let container = transitionContext.containerView | |
// 表示させるViewControllerを格納するための変数を定義する | |
var detailView: UIView! | |
// Case1: 進む場合 | |
if presenting { | |
container.addSubview(toView) | |
detailView = toView | |
// Case2: 戻る場合 | |
} else { | |
container.insertSubview(toView, belowSubview: fromView) | |
detailView = fromView | |
} | |
// 遷移先のViewControllerに配置したUIImageViewのタグ値から、カスタムトランジション時に動かすUIImageViewの情報を取得する | |
// ※ 今回はDetailViewController内に配置したtransitionTargetImageViewが該当する | |
guard let targetImageView = detailView.viewWithTag(customAnimatorTag) as? UIImageView else { | |
return | |
} | |
targetImageView.image = originImage | |
targetImageView.alpha = 0 | |
// カスタムトランジションでViewControllerを表示させるViewの表示に関する値を格納する変数 | |
var toViewAlpha: CGFloat! | |
var beforeTransitionImageViewFrame: CGRect! | |
var afterTransitionImageViewFrame: CGRect! | |
var afterTransitionViewAlpha: CGFloat! | |
// Case1: 進む場合 | |
if presenting { | |
toViewAlpha = 0 | |
beforeTransitionImageViewFrame = originFrame | |
// MEMO: 詳細画面の初期配置位置に重なる様にframe値を設定する | |
// targetImageView.frameを設定するとStoryboardの値が基準となる | |
afterTransitionImageViewFrame = CGRect( | |
x: 0, | |
y: 0, | |
width: UIScreen.main.bounds.width, | |
height: UIScreen.main.bounds.width * 0.75 | |
) | |
afterTransitionViewAlpha = 1 | |
// Case2: 戻る場合 | |
} else { | |
toViewAlpha = 1 | |
beforeTransitionImageViewFrame = targetImageView.frame | |
afterTransitionImageViewFrame = originFrame | |
afterTransitionViewAlpha = 0 | |
} | |
// 遷移時に動かすUIImageViewを追加する | |
let transitionImageView = UIImageView(frame: beforeTransitionImageViewFrame) | |
transitionImageView.image = originImage | |
transitionImageView.contentMode = .scaleAspectFill | |
transitionImageView.clipsToBounds = true | |
container.addSubview(transitionImageView) | |
// 遷移先のViewのアルファ値を反映する | |
toView.alpha = toViewAlpha | |
toView.layoutIfNeeded() | |
UIView.animate(withDuration: duration, delay: 0.00, options: [.curveEaseInOut], animations: { | |
transitionImageView.frame = afterTransitionImageViewFrame | |
detailView.alpha = afterTransitionViewAlpha | |
}, completion: { _ in | |
transitionContext.completeTransition(!transitionContext.transitionWasCancelled) | |
transitionImageView.removeFromSuperview() | |
targetImageView.alpha = 1 | |
}) | |
} | |
} | |
// 2. DetailInteractorクラス実装例 | |
import Foundation | |
import UIKit | |
class DetailInteractor: UIPercentDrivenInteractiveTransition { | |
// UINavigationControllerを格納するための変数 | |
private var navigationController: UINavigationController | |
// 画面遷移を完了するか否かの判定フラグ | |
private var shouldCompleteTransition = false | |
// 画面遷移の操作中であるか否かの判定フラグ | |
var transitionInProgress = false | |
// MARK: - Initializer | |
init?(attachTo viewController: UIViewController) { | |
if let nav = viewController.navigationController { | |
self.navigationController = nav | |
super.init() | |
prepareGestureRecognizerInView(viewController.view) | |
} else { | |
return nil | |
} | |
} | |
// MARK: - Private Function | |
// UIScreenEdgePanGestureRecognizerが発火した際のアクションを定義する | |
@objc private func handleGesture(_ gesture: UIScreenEdgePanGestureRecognizer) { | |
// X軸方向の変化量を算出する | |
let viewTranslation = gesture.translation(in: gesture.view?.superview) | |
let progress = viewTranslation.x / self.navigationController.view.frame.width | |
// UIScreenEdgePanGestureRecognizerの状態によって動き方の場合分けにする | |
switch gesture.state { | |
// 1.開始時 | |
case .began: | |
transitionInProgress = true | |
navigationController.popViewController(animated: true) | |
break | |
// 2.変更時 | |
case .changed: | |
shouldCompleteTransition = (progress > 0.5) | |
update(progress) | |
break | |
// 3.キャンセル時 | |
case .cancelled: | |
transitionInProgress = false | |
cancel() | |
break | |
// 4.終了時 | |
case .ended: | |
transitionInProgress = false | |
shouldCompleteTransition ? finish() : cancel() | |
break | |
default: | |
print("This state is unsupported to UIScreenEdgePanGestureRecognizer.") | |
return | |
} | |
} | |
// UIScreenEdgePanGestureRecognizerを追加する | |
private func prepareGestureRecognizerInView(_ view: UIView) { | |
let gesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(self.handleGesture(_:))) | |
gesture.edges = .left | |
view.addGestureRecognizer(gesture) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment