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
460
Other Decks in Technology
See All in Technology
クラウド開発の舞台裏とSRE文化の醸成 / SRE NEXT 2025 Lunch Session
kazeburo
1
580
全部AI、全員Cursor、ドキュメント駆動開発 〜DevinやGeminiも添えて〜
rinchsan
10
5.1k
Snowflake Intelligenceという名のAI Agentが切り開くデータ活用の未来とその実現に必要なこと@SnowVillage『Data Management #1 Summit 2025 Recap!!』
ryo_suzuki
1
160
SREのためのeBPF活用ステップアップガイド
egmc
2
1.3k
安定した基盤システムのためのライブラリ選定
kakehashi
PRO
3
130
[SRE NEXT 2025] すみずみまで暖かく照らすあなたの太陽でありたい
carnappopper
2
470
セキュアな社内Dify運用と外部連携の両立 ~AIによるAPIリスク評価~
zozotech
PRO
0
120
CDK Vibe Coding Fes
tomoki10
1
630
Introduction to Sansan, inc / Sansan Global Development Center, Inc.
sansan33
PRO
0
2.7k
LLM拡張解体新書/llm-extension-deep-dive
oracle4engineer
PRO
23
6.2k
ソフトウェアテストのAI活用_ver1.25
fumisuke
1
610
データ戦略部門 紹介資料
sansan33
PRO
1
3.3k
Featured
See All Featured
Making Projects Easy
brettharned
116
6.3k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
8
340
Stop Working from a Prison Cell
hatefulcrawdad
271
21k
Scaling GitHub
holman
460
140k
A better future with KSS
kneath
238
17k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
15
1.6k
Docker and Python
trallard
45
3.5k
How GitHub (no longer) Works
holman
314
140k
Unsuck your backbone
ammeep
671
58k
Producing Creativity
orderedlist
PRO
346
40k
Build The Right Thing And Hit Your Dates
maggiecrowley
37
2.8k
Optimizing for Happiness
mojombo
379
70k
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ΞϓϦͷ։ൃͷ͓ࣄืूதͰ͢ʂ