-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathModalCardViewController.swift
More file actions
162 lines (138 loc) · 5.48 KB
/
ModalCardViewController.swift
File metadata and controls
162 lines (138 loc) · 5.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//
// ModalCardViewController.swift
// DevsignNavigationTransitions
//
// Created by Bryan Clark on 5/7/19.
// Copyright © 2019 Bryan Clark. All rights reserved.
//
import UIKit
import Cartography
// When animating in/out of ModalCardViewController,
// we need to know what type of transition is going on,
// so we can animate properly.
// Think of this as a modal version of UINavigationController.Operation
fileprivate enum ModalTransitionType {
case presentation, dismissal
}
class ModalCardViewController: UIViewController {
private let cardView = UIView()
private let dismissButton = UIButton()
private let dismissTapView = UIView()
fileprivate var currentModalTransitionType: ModalTransitionType? = nil
fileprivate static let overlayBackgroundColor = UIColor.black.withAlphaComponent(0.4)
init() {
super.init(nibName: nil, bundle: nil)
self.transitioningDelegate = self
self.modalPresentationStyle = .overFullScreen
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = ModalCardViewController.overlayBackgroundColor
// If you tap the gray background, the card dismisses
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissButtonTapped))
self.dismissTapView.addGestureRecognizer(tapGesture)
self.view.addSubview(self.dismissTapView)
constrain(self.dismissTapView) { $0.edges == $0.superview!.edges }
// Let's add the rounded-corner white card
self.cardView.backgroundColor = .white
self.cardView.layer.cornerRadius = 12
self.cardView.clipsToBounds = true
self.view.addSubview(self.cardView)
constrain(self.cardView) {
$0.leading == $0.superview!.safeAreaLayoutGuide.leading + 8
$0.trailing == $0.superview!.safeAreaLayoutGuide.trailing - 8
$0.bottom == $0.superview!.safeAreaLayoutGuide.bottom - 8
$0.height == 300
}
// ...and the dismiss button
self.dismissButton.setTitle("Dismiss", for: .normal)
self.dismissButton.setTitleColor(self.view.tintColor, for: .normal)
self.dismissButton.addTarget(self, action: #selector(dismissButtonTapped), for: .touchUpInside)
self.cardView.addSubview(self.dismissButton)
constrain(self.dismissButton) {
$0.leading == $0.superview!.leading
$0.trailing == $0.superview!.trailing
$0.bottom == $0.superview!.bottom
$0.height == 50
}
}
@objc private func dismissButtonTapped() {
self.presentingViewController?.dismiss(animated: true, completion: nil)
}
}
extension ModalCardViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let result = (presented == self) ? self : nil
result?.currentModalTransitionType = .presentation
return result
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let result = (dismissed == self) ? self : nil
result?.currentModalTransitionType = .dismissal
return result
}
}
extension ModalCardViewController: UIViewControllerAnimatedTransitioning {
private var transitionDuration: TimeInterval {
guard let transitionType = self.currentModalTransitionType else { fatalError() }
switch transitionType {
case .presentation:
return 0.44
case .dismissal:
return 0.32
}
}
public func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?
) -> TimeInterval {
return transitionDuration
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let transitionType = self.currentModalTransitionType else { fatalError() }
// Here's the state we'd be in when the card is offscreen
let cardOffscreenState = {
let offscreenY = self.view.bounds.height - self.cardView.frame.minY + 20
self.cardView.transform = CGAffineTransform.identity.translatedBy(x: 0, y: offscreenY)
self.view.backgroundColor = .clear
}
// ...and here's the state of things when the card is onscreen.
let presentedState = {
self.cardView.transform = CGAffineTransform.identity
self.view.backgroundColor = ModalCardViewController.overlayBackgroundColor
}
// We want different animation timing, based on whether we're presenting or dismissing.
let animator: UIViewPropertyAnimator
switch transitionType {
case .presentation:
animator = UIViewPropertyAnimator(duration: transitionDuration, dampingRatio: 0.82)
case .dismissal:
animator = UIViewPropertyAnimator(duration: transitionDuration, curve: UIView.AnimationCurve.easeIn)
}
switch transitionType {
case .presentation:
// We need to add the modal to the view hierarchy,
// and perform the animation.
let toView = transitionContext.view(forKey: .to)!
UIView.performWithoutAnimation(cardOffscreenState)
transitionContext.containerView.addSubview(toView)
animator.addAnimations(presentedState)
case .dismissal:
// The modal is already in the view hierarchy,
// so we just perform the animation.
animator.addAnimations(cardOffscreenState)
}
// When the animation finishes,
// we tell the system that the animation has completed,
// and clear out our transition type.
animator.addCompletion { (position) in
assert(position == .end)
transitionContext.completeTransition(true)
self.currentModalTransitionType = nil
}
// ... and here's where we kick off the animation:
animator.startAnimation()
}
}