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.3k
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
430
Other Decks in Technology
See All in Technology
Java x Spring Boot Warm up
kazu_kichi_67
2
490
10分でわかるfreee エンジニア向け会社説明資料
freee
18
520k
Oracle Base Database Service 技術詳細
oracle4engineer
PRO
5
49k
GitHub Universe: Evaluating RAG apps in GitHub Actions
pamelafox
0
170
Commitment vs Harrisonism - Keynote for Scrum Niseko 2024
miholovesq
6
1.1k
LeSSに潜む「隠れWF病」とその処方箋
lycorptech_jp
PRO
2
120
君は隠しイベントを見つけれるか?
mujyun
0
290
visionOSでの空間表現実装とImmersive Video表示について / ai-immersive-visionos
cyberagentdevelopers
PRO
1
110
なんで、私がAWS Heroに!? 〜社外の広い世界に一歩踏み出そう〜
minorun365
PRO
6
1.1k
AWS re:Inventを徹底的に楽しむためのTips / Tips for thoroughly enjoying AWS re:Invent
yuj1osm
1
560
分布で見る効果検証入門 / ai-distributional-effect
cyberagentdevelopers
PRO
4
700
【若手エンジニア応援LT会】AWSで繋がり、共に成長! ~コミュニティ活動と新人教育への挑戦~
kazushi_ohata
0
180
Featured
See All Featured
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
37
1.8k
Thoughts on Productivity
jonyablonski
67
4.3k
Imperfection Machines: The Place of Print at Facebook
scottboms
264
13k
Typedesign – Prime Four
hannesfritz
39
2.4k
Building Flexible Design Systems
yeseniaperezcruz
327
38k
Fireside Chat
paigeccino
32
3k
Fashionably flexible responsive web design (full day workshop)
malarkey
404
65k
Java REST API Framework Comparison - PWX 2021
mraible
PRO
28
7.9k
The Pragmatic Product Professional
lauravandoore
31
6.3k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
159
15k
Happy Clients
brianwarren
97
6.7k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
228
52k
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ΞϓϦͷ։ൃͷ͓ࣄืूதͰ͢ʂ