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
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Kazuya Hirobe
May 24, 2018
Technology
1.5k
0
Share
iPhoneXのホームに戻るみたいなDissmisアニメーション
iOSアプリ開発でiPhoneXのホームに戻るみたいなDissmisアニメーションを実装する方法を説明します。
Kazuya Hirobe
May 24, 2018
More Decks by Kazuya Hirobe
See All by Kazuya Hirobe
もっとFluidでRedirectableなモーダル表示アニメーション
hirobe
3
480
Other Decks in Technology
See All in Technology
15年メンテしてきたdotfilesから開発トレンドを振り返る 2011 - 2026
giginet
PRO
2
260
Zephyr(RTOS)でARMとRISC-Vのコア間通信をしてみた
iotengineer22
0
120
Blue/Green Deployment を用いた PostgreSQL のメジャーバージョンアップ
kkato1
1
210
VSCode中心だった自分がターミナル沼に入門した話
sanogemaru
0
890
Podcast配信で広がったアウトプットの輪~70人と音声発信してきた7年間~/outputconf_01
fortegp05
0
180
遊びで始めたNew Relic MCP、気づいたらChatOpsなオブザーバビリティボットができてました/From New Relic MCP to a ChatOps Observability Bot
aeonpeople
1
150
OpenClawでPM業務を自動化
knishioka
2
360
AI時代のシステム開発者の仕事_20260328
sengtor
0
320
AIエージェント勉強会第3回 エージェンティックAIの時代がやってきた
ymiya55
0
210
脳が溶けた話 / Melted Brain
keisuke69
1
1.2k
BFCacheを活用して無限スクロールのUX を改善した話
apple_yagi
0
140
開発チームとQAエンジニアの新しい協業モデル -年末調整開発チームで実践する【QAリード施策】-
qa
0
670
Featured
See All Featured
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
The Illustrated Children's Guide to Kubernetes
chrisshort
51
52k
Faster Mobile Websites
deanohume
310
31k
What Being in a Rock Band Can Teach Us About Real World SEO
427marketing
0
200
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.7k
[RailsConf 2023] Rails as a piece of cake
palkan
59
6.4k
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
500
Color Theory Basics | Prateek | Gurzu
gurzu
0
270
Intergalactic Javascript Robots from Outer Space
tanoku
273
27k
The AI Revolution Will Not Be Monopolized: How open-source beats economies of scale, even for LLMs
inesmontani
PRO
3
3.2k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
12
1.1k
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
110
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ΞϓϦͷ։ൃͷ͓ࣄืूதͰ͢ʂ