Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
iPhoneXのホームに戻るみたいなDissmisアニメーション
Search
Kazuya Hirobe
May 24, 2018
Technology
0
1.4k
iPhoneXのホームに戻るみたいなDissmisアニメーション
iOSアプリ開発でiPhoneXのホームに戻るみたいなDissmisアニメーションを実装する方法を説明します。
Kazuya Hirobe
May 24, 2018
Tweet
Share
More Decks by Kazuya Hirobe
See All by Kazuya Hirobe
もっとFluidでRedirectableなモーダル表示アニメーション
hirobe
3
440
Other Decks in Technology
See All in Technology
宇宙ベンチャーにおける最近の情シス取り組みについて
axelmizu
0
110
DevFest 2024 Incheon / Songdo - Compose UI 조합 심화
wisemuji
0
120
権威ドキュメントで振り返る2024 #年忘れセキュリティ2024
hirotomotaguchi
2
760
フロントエンド設計にモブ設計を導入してみた / 20241212_cloudsign_TechFrontMeetup
bengo4com
0
1.9k
PHPからGoへのマイグレーション for DMMアフィリエイト
yabakokobayashi
1
170
C++26 エラー性動作
faithandbrave
2
780
成果を出しながら成長する、アウトプット駆動のキャッチアップ術 / Output-driven catch-up techniques to grow while producing results
aiandrox
0
360
ブラックフライデーで購入したPixel9で、Gemini Nanoを動かしてみた
marchin1989
1
540
LINEスキマニにおけるフロントエンド開発
lycorptech_jp
PRO
0
330
Qiita埋め込み用スライド
naoki_0531
0
5.1k
新機能VPCリソースエンドポイント機能検証から得られた考察
duelist2020jp
0
230
祝!Iceberg祭開幕!re:Invent 2024データレイク関連アップデート10分総ざらい
kniino
3
320
Featured
See All Featured
Music & Morning Musume
bryan
46
6.2k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
2
290
BBQ
matthewcrist
85
9.4k
The Power of CSS Pseudo Elements
geoffreycrofte
73
5.4k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
111
49k
jQuery: Nuts, Bolts and Bling
dougneiner
61
7.5k
GraphQLとの向き合い方2022年版
quramy
44
13k
Facilitating Awesome Meetings
lara
50
6.1k
Principles of Awesome APIs and How to Build Them.
keavy
126
17k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
159
15k
The Cost Of JavaScript in 2023
addyosmani
45
7k
[RailsConf 2023] Rails as a piece of cake
palkan
53
5k
Transcript
iPhoneXͷϗʔϜʹΔ Έ͍ͨͳ DismissΞχϝʔγϣϯ @hirobe 2018.5.20
ࣗݾհ ෦Ұ ( twitter : @hirobe ) גࣜձࣾBunguu iOSΞϓϦɺMacOSΞϓϦͷ։ൃΛͬͯ·͢ ࠓճͷιʔείʔυ:
https://github.com/hirobe/SwipeZoomOutSample
Home Animation of iPhone X ը૾ϓϨϏϡʔΛด͡Δͱ͖ʹ ͜Μͳײ͡Ͱด͍ͨ͡
ͷྲྀΕ • جૅΛ݉ͶͯPresentΞχϝʔγϣϯ • iPhoneXࣜͷDismissΞχϝʔγϣϯ
Present(දࣔ)Ξχϝʔγϣϯ ը૾αϜωΠϧΛλοϓͨ͠Β֦େ͢Δ From To
Present(දࣔ)Ξχϝʔγϣϯ • UIViewControllerTransitionDelegate, UIViewControllerAnimatedTransitioningΛ࣮ • ։࣌͘ʹtransitionDelegateʹηοτ override func collectionView(_ collectionView:
UICollectionView, didSelectItemAt indexPath: IndexPath) { : let storyboard = UIStoryboard(name: "Main", bundle: nil) guard let vc = storyboard.instantiateViewController(withIdentifier: "DetailViewController") as? DetailViewController else { return } let nav = UINavigationController(rootViewController: vc) nav.isNavigationBarHidden = true nav.modalPresentationStyle = .custom nav.transitioningDelegate = vc.modalTransition self.present(nav, animated: true) }
Present(දࣔ) Ξχϝʔγϣϯ UIViewControllerTransitionDelegate, UIViewControllerAnimatedTransitioning ΞχϝʔγϣϯΛهड़ class DetailModalTransition : NSObject, UIViewControllerTransitioningDelegate,
UIViewControllerAnimatedTransitioning { var isForPresented:Bool = true func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.isForPresented = true return self } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.isForPresented = false return self } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return isForPresented ? 0.2 : 0.4 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { if isForPresented { // present presentAnimation(transitionContext) } else { // dissmis dissmisAnimation(transitionContext) } }
Present(දࣔ) Ξχϝʔγϣϯ UIViewControllerTransitionDelegate, UIViewControllerAnimatedTransitioning ΞχϝʔγϣϯΛهड़ class DetailModalTransition : NSObject, UIViewControllerTransitioningDelegate,
UIViewControllerAnimatedTransitioning { var isForPresented:Bool = true func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.isForPresented = true return self } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.isForPresented = false return self } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return isForPresented ? 0.2 : 0.4 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { if isForPresented { // present presentAnimation(transitionContext) } else { // dissmis dissmisAnimation(transitionContext) } } • animationController(forPresented: source) • animationController(forDismissed: source) • present, dismiss༻ͷΫϥεʢࣗࣗʣΛฦ͢ • isForPresentedΛηοτ
Present(දࣔ) Ξχϝʔγϣϯ UIViewControllerTransitionDelegate, UIViewControllerAnimatedTransitioning ΞχϝʔγϣϯΛهड़ class DetailModalTransition : NSObject, UIViewControllerTransitioningDelegate,
UIViewControllerAnimatedTransitioning { var isForPresented:Bool = true func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.isForPresented = true return self } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.isForPresented = false return self } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return isForPresented ? 0.2 : 0.4 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { if isForPresented { // present presentAnimation(transitionContext) } else { // dissmis dissmisAnimation(transitionContext) } } • transitionDuraiton Ξχϝʔγϣϯͷ࣮ߦ࣌ؒ(Duration)Λฦ͢ • animateTransition Ξχϝʔγϣϯͷ࣮ࢪ
• ç func presentAnimation(_ transitionContext: UIViewControllerContextTransitioning) { let containerView =
transitionContext.containerView guard let nav = transitionContext.viewController(forKey: .to) as? UINavigationController, let detailVC:DetailViewController = nav.visibleViewController as? DetailViewController else { fatalError() } let fromImageRect = detailVC.parentImageViewRect let toImageRect = detailVC.detailImageRect(containerView: containerView, safeAreaInsets: self.safeAreaInsets(transitionContext: transitionContext)) // imageΛҠಈ͢ΔͨΊͷviewΛ࡞ let imageView:UIImageView = UIImageView(frame: fromImageRect) imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill imageView.image = detailVC.parentImage containerView.addSubview(imageView) UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseOut], animations: { () -> Void in imageView.frame = toImageRect }) { (finished) -> Void in containerView.addSubview(nav.view) imageView.removeFromSuperview() transitionContext.completeTransition(true) } }
• ç func presentAnimation(_ transitionContext: UIViewControllerContextTransitioning) { let containerView =
transitionContext.containerView guard let nav = transitionContext.viewController(forKey: .to) as? UINavigationController, let detailVC:DetailViewController = nav.visibleViewController as? DetailViewController else { fatalError() } let fromImageRect = detailVC.parentImageViewRect let toImageRect = detailVC.detailImageRect(containerView: containerView, safeAreaInsets: self.safeAreaInsets(transitionContext: transitionContext)) // imageΛҠಈ͢ΔͨΊͷviewΛ࡞ let imageView:UIImageView = UIImageView(frame: fromImageRect) imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill imageView.image = detailVC.parentImage containerView.addSubview(imageView) UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseOut], animations: { () -> Void in imageView.frame = toImageRect }) { (finished) -> Void in containerView.addSubview(nav.view) imageView.removeFromSuperview() transitionContext.completeTransition(true) } } • transitionContext͔Βऔಘ • ͱͳΔcontainerView • ભҠઌͷViewController ViewControllerͷΫϥεܕࢦఆ͢Δ
• ç func presentAnimation(_ transitionContext: UIViewControllerContextTransitioning) { let containerView =
transitionContext.containerView guard let nav = transitionContext.viewController(forKey: .to) as? UINavigationController, let detailVC:DetailViewController = nav.visibleViewController as? DetailViewController else { fatalError() } let fromImageRect = detailVC.parentImageViewRect let toImageRect = detailVC.detailImageRect(containerView: containerView, safeAreaInsets: self.safeAreaInsets(transitionContext: transitionContext)) // imageΛҠಈ͢ΔͨΊͷviewΛ࡞ let imageView:UIImageView = UIImageView(frame: fromImageRect) imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill imageView.image = detailVC.parentImage containerView.addSubview(imageView) UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseOut], animations: { () -> Void in imageView.frame = toImageRect }) { (finished) -> Void in containerView.addSubview(nav.view) imageView.removeFromSuperview() transitionContext.completeTransition(true) } } Ξχϝʔγϣϯʹ͏ͷܭࢉ ɾ։࢝࣌ͷ࠲ඪͱऴྃ࣌ͷ࠲ඪ ิ • ToଆͷVCදࣔલͳͷͰࣗͰ࠲ඪܭࢉ From To
• ç func presentAnimation(_ transitionContext: UIViewControllerContextTransitioning) { let containerView =
transitionContext.containerView guard let nav = transitionContext.viewController(forKey: .to) as? UINavigationController, let detailVC:DetailViewController = nav.visibleViewController as? DetailViewController else { fatalError() } let fromImageRect = detailVC.parentImageViewRect let toImageRect = detailVC.detailImageRect(containerView: containerView, safeAreaInsets: self.safeAreaInsets(transitionContext: transitionContext)) // imageΛҠಈ͢ΔͨΊͷviewΛ࡞ let imageView:UIImageView = UIImageView(frame: fromImageRect) imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill imageView.image = detailVC.parentImage containerView.addSubview(imageView) UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseOut], animations: { () -> Void in imageView.frame = toImageRect }) { (finished) -> Void in containerView.addSubview(nav.view) imageView.removeFromSuperview() transitionContext.completeTransition(true) } } containerViewʹΞχϝʔγϣϯ͢ΔViewΛషΓ͚ • ࠲ඪΛࢦఆ͠ͳ͕Β
• ç func presentAnimation(_ transitionContext: UIViewControllerContextTransitioning) { let containerView =
transitionContext.containerView guard let nav = transitionContext.viewController(forKey: .to) as? UINavigationController, let detailVC:DetailViewController = nav.visibleViewController as? DetailViewController else { fatalError() } let fromImageRect = detailVC.parentImageViewRect let toImageRect = detailVC.detailImageRect(containerView: containerView, safeAreaInsets: self.safeAreaInsets(transitionContext: transitionContext)) // imageΛҠಈ͢ΔͨΊͷviewΛ࡞ let imageView:UIImageView = UIImageView(frame: fromImageRect) imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill imageView.image = detailVC.parentImage containerView.addSubview(imageView) UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseOut], animations: { () -> Void in imageView.frame = toImageRect }) { (finished) -> Void in containerView.addSubview(nav.view) imageView.removeFromSuperview() transitionContext.completeTransition(true) } } Ξχϝʔγϣϯͷ࣮ߦʢޙॲཧʣ imageViewͷframeΛมߋ
Present(දࣔ)Ξχϝʔγϣϯ • UIViewControllerTransitionDelegate, UIViewControllerAnimatedTransitioning Λ࣮ ->͜͜ʹΞχϝʔγϣϯΛॻ͘ • ։࣌͘ʹtransitionDelegateʹηοτ
iPhoneXϗʔϜʹΔΈ͍ͨͳ DissmisΛߟ͑Δ • εϫΠϓதυϥοάʹै + ॖখ • ࢦΛͨ͠Βࢦఆ࠲ඪʹΒ͔ʹҠಈ + ॖখ
Swipe Animation Animation To From Swipe
Swipeૢ࡞தPanGestureͰ • εϫΠϓͰࢦ͕λον͍ͯ͠Δؒυϥοάಈ࡞ • ࢦ͕ΕͨΒɺͦͷҐஔ͔ΒΞχϝʔγϣϯ։࢝ @objc func handlePanGesture(_ sender: UIPanGestureRecognizer){
let point: CGPoint = sender.translation(in: self.view) let velocity = sender.velocity(in: self.view) let per = fabs(point.y) / self.view.frame.size.height switch (sender.state) { case .cancelled, .failed: resetPosition() case .changed: self.imageView.transform = CGAffineTransform(scaleX: 1.0 - per, y: 1.0 - per) .concatenating( CGAffineTransform(translationX: point.x, y: point.y) ) case .ended: if per > 0.1 || fabs(velocity.y) > 1000 { self.modalTransition.isForPresented = false self.modalTransition.swipeScale = 1.0 - per self.modalTransition.swipePoint = CGPoint(x: point.x , y: point.y ) self.modalTransition.swipeVelocity = velocity self.dismiss(animated: true, completion: nil) } else { resetPosition() } default: break } }
Swipeૢ࡞தPanGestureͰ • εϫΠϓͰࢦ͕λον͍ͯ͠Δؒυϥοάಈ࡞ • ࢦ͕ΕͨΒɺͦͷҐஔ͔ΒΞχϝʔγϣϯ։࢝ @objc func handleVerticalPanGesture(_ sender: UIPanGestureRecognizer){
let point: CGPoint = sender.translation(in: self.view) let velocity = sender.velocity(in: self.view) let per = fabs(point.y) / self.view.frame.size.height switch (sender.state) { case .cancelled, .failed: resetPosition() case .changed: self.imageView.transform = CGAffineTransform(scaleX: 1.0 - per, y: 1.0 - per) .concatenating( CGAffineTransform(translationX: point.x, y: point.y) ) case .ended: if per > 0.1 || fabs(velocity.y) > 1000 { self.modalTransition.isForPresented = false self.modalTransition.swipeScale = 1.0 - per self.modalTransition.swipePoint = CGPoint(x: point.x , y: point.y ) self.modalTransition.swipeVelocity = velocity self.dismiss(animated: true, completion: nil) } else { resetPosition() } default: break } } υϥοάதɺॖখͱը૾ͷҠಈ
Swipeૢ࡞தPanGestureͰ • εϫΠϓͰࢦ͕λον͍ͯ͠Δؒυϥοάಈ࡞ • ࢦ͕ΕͨΒɺͦͷҐஔ͔ΒΞχϝʔγϣϯ։࢝ @objc func handleVerticalPanGesture(_ sender: UIPanGestureRecognizer){
let point: CGPoint = sender.translation(in: self.view) let velocity = sender.velocity(in: self.view) let per = fabs(point.y) / self.view.frame.size.height switch (sender.state) { case .cancelled, .failed: resetPosition() case .changed: self.imageView.transform = CGAffineTransform(scaleX: 1.0 - per, y: 1.0 - per) .concatenating( CGAffineTransform(translationX: point.x, y: point.y) ) case .ended: if per > 0.1 || fabs(velocity.y) > 1000 { self.modalTransition.isForPresented = false self.modalTransition.swipeScale = 1.0 - per self.modalTransition.swipePoint = CGPoint(x: point.x , y: point.y ) self.modalTransition.swipeVelocity = velocity self.dismiss(animated: true, completion: nil) } else { resetPosition() } default: break } } ࢦΛͨ͠ΒDissmissΞχϝʔγϣϯΛ։࢝ ɾࢦΛͨ͠ҐஔɺॖখɺεϫΠϓͷΛ͢
ࢦΛͨ͋͠ͱͷ Β͔ͳΞχϝʔγϣϯͱ • ͦͷ··ΔͱΧΫͬͱͨ͠ಈ͖ʹ • EasyIn, EasyOutͰͩΊ → Swipe࣌ͷಈ͖ΛΞχϝʔγϣϯʹ͍ͨ͠ Swipe
curveLiner NG Swipe curveEasyInOut NG
Spring Animation ͩʂ • ύϥϝλDamping(ੑ)ͱVelocity(ॳظ) • δΣενϟʔ͔ΒVelocity()Λ͢ͱྑͦ͞͏ Swipe Dumping Animation
To From Velocity : 999 Velocity : -999(?) Dumping : 1.0 UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: velocityY , animations: {})
Velocityͷม Gesutreͷvelocity : 1ඵؒʹҠಈ͢Δpt Spring Animationͷvelocity: 1.0 = 1ඵؒͰΞχϝʔγϣϯͷڑʹ౸ୡ͢Δʢॳظʣ ྫɿڑ200ptͰɺॳظΛ100pt/sʹ͍ͨ͠ͳΒ0.5
+/- ҙ ٯํ ॱํ ܭࢉࣜ sv.y = gv.y / (from.y - to.y) sv.x = gv.x / (from.x - to.x) sv: Spring AnimationͷVelocity gv: GestureͷVelocity
Dampingύϥϝʔλ • Dampingόωͷੑ • 0.0(ॊΒ͔͍)-1.0(ߗ͍) • 0.9ʙ1.0͘Β͍Ͱࢦఆ
UIViewͷߏ • Ξχϝʔγϣϯͷ࣠͝ͱʹUIViewΛ͚Δ • X, YSpring AnimationͰҠಈ • ը૾ͷେ͖͞curveLinerͰॖখ Y࣠Ҡಈ
X࣠Ҡಈ ॖখ
func dissmisAnimation(_ transitionContext: UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView guard
let nav = transitionContext.viewController(forKey: .from) as? UINavigationController, let detailVC:DetailViewController = nav.visibleViewController as? DetailViewController else { fatalError() } let fromImageRect = detailVC.detailImageRect(containerView: containerView, safeAreaInsets: self.safeAreaInsets(transitionContext: transitionContext)) let toImageRect = detailVC.parentImageViewRect // ԼΛӅͨ͢ΊͷviewΛ࡞ let backgroundView:UIView = UIView(frame:containerView.bounds) containerView.addSubview(backgroundView) backgroundView.backgroundColor = UIColor.white // imageΛҠಈ͢ΔͨΊͷviewΛ࡞ let yMoveView:UIView = UIView(frame:containerView.bounds) let xMoveView:UIView = UIView(frame:containerView.bounds) let imageView:UIImageView = UIImageView(frame: fromImageRect) imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill imageView.image = detailVC.parentImage containerView.addSubview(yMoveView) yMoveView.addSubview(xMoveView) xMoveView.addSubview(imageView) // Ξχϝʔγϣϯ nav.view.isHidden = true backgroundView.alpha = 1.0 imageView.frame = fromImageRect let velocityX = min(self.swipeVelocity.x / (toImageRect.midX - fromImageRect.midX) , 10000.0) let velocityY = min(self.swipeVelocity.y / (toImageRect.midY - fromImageRect.midY) , 10000.0)
// x࣠ҠಈΞχϝʔγϣϯ UIView.animate(withDuration: transitionDuration(using: transitionContext) , delay: 0, usingSpringWithDamping: 0.95,
initialSpringVelocity: velocityX, animations: { () -> Void in xMoveView.frame.origin = CGPoint(x: toImageRect.midX - fromImageRect.midX, y:0) }) // y࣠ҠಈΞχϝʔγϣϯ UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 0.95, initialSpringVelocity: velocityY , animations: { () -> Void in yMoveView.frame.origin = CGPoint(x:0, y: toImageRect.midY - fromImageRect.midY) }) // ॖখΞχϝʔγϣϯ UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveLinear], animations: { () -> Void in imageView.frame.size = toImageRect.size imageView.center = CGPoint(x:fromImageRect.midX, y:fromImageRect.midY) backgroundView.alpha = 0.0 }) { (finished) -> Void in yMoveView.isHidden = true nav.view.removeFromSuperview() yMoveView.removeFromSuperview() backgroundView.removeFromSuperview() transitionContext.completeTransition(true) } }
Ͱ͖͕͋Γ GitHub: https://github.com/hirobe/ SwipeZoomOutSample ฐࣾiOSΞϓϦͷ։ൃͷ͓ࣄืूதͰ͢ʂ