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
470
Other Decks in Technology
See All in Technology
英語は話せません!それでも海外チームと信頼関係を作るため、対話を重ねた2ヶ月間のまなび
niioka_97
0
120
「AI駆動PO」を考えてみる - 作る速さから価値のスループットへ:検査・適応で未来を開発 / AI-driven product owner. scrummat2025
yosuke_nagai
4
600
社内お問い合わせBotの仕組みと学び
nish01
0
390
LLM時代にデータエンジニアの役割はどう変わるか?
ikkimiyazaki
1
310
実装で解き明かす並行処理の歴史
zozotech
PRO
1
370
PLaMoの事後学習を支える技術 / PFN LLMセミナー
pfn
PRO
9
3.8k
SwiftUIのGeometryReaderとScrollViewを基礎から応用まで学び直す:設計と活用事例
fumiyasac0921
0
140
Where will it converge?
ibknadedeji
0
180
AI Agentと MCP Serverで実現する iOSアプリの 自動テスト作成の効率化
spiderplus_cb
0
500
Exadata Database Service on Dedicated Infrastructure(ExaDB-D) UI スクリーン・キャプチャ集
oracle4engineer
PRO
2
5.4k
Goに育てられ開発者向けセキュリティ事業を立ち上げた僕が今向き合う、AI × セキュリティの最前線 / Go Conference 2025
flatt_security
0
350
Oracle Base Database Service 技術詳細
oracle4engineer
PRO
11
77k
Featured
See All Featured
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
54
3k
Docker and Python
trallard
46
3.6k
I Don’t Have Time: Getting Over the Fear to Launch Your Podcast
jcasabona
33
2.5k
VelocityConf: Rendering Performance Case Studies
addyosmani
332
24k
Building Applications with DynamoDB
mza
96
6.6k
Git: the NoSQL Database
bkeepers
PRO
431
66k
Intergalactic Javascript Robots from Outer Space
tanoku
273
27k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
285
14k
Art, The Web, and Tiny UX
lynnandtonic
303
21k
Optimizing for Happiness
mojombo
379
70k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Rails Girls Zürich Keynote
gr2m
95
14k
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ΞϓϦͷ։ൃͷ͓ࣄืूதͰ͢ʂ