forked from SwiftKickMobile/SwiftMessages
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSwiftMessagesSegue.swift
354 lines (306 loc) · 15.7 KB
/
SwiftMessagesSegue.swift
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
//
// SwiftMessagesSegue.swift
// SwiftMessages
//
// Created by Timothy Moose on 5/30/18.
// Copyright © 2018 SwiftKick Mobile. All rights reserved.
//
import UIKit
/**
`SwiftMessagesSegue` is a configurable subclass of `UIStoryboardSegue` that utilizes
SwiftMessages to present and dismiss modal view controllers. It performs these transitions by
becoming your view controller's `transitioningDelegate` and calling SwiftMessage's `show()`
and `hide()` under the hood.
To use `SwiftMessagesSegue` with Interface Builder, control-drag a segue, then select
"swift messages" from the Segue Type dialog. This configures a default transition. There are
two ways to further configure the transition by setting configuration options on `SwiftMessagesSegue`.
First, you may override `prepare(for:sender:)` in the presenting view controller and downcast the
segue to `SwiftMessagesSegue`. Second, and recommended, you may subclass `SwiftMessagesSegue` and
override `init(identifier:source:destination:)`. Subclasses will automatically appear in the segue
type dialog using an auto-generated name (for example, the name for "VeryNiceSegue" would be "very nice").
The SwiftMessagesSegueExtras framework contains several pre-configured subclasses: `TopMessageSegue`,
`BottomMessageSegue`, `TopCardSegue`, `BottomCardSegue`, `TopTabSegue`, `BottomTabSegue`, and
`CenteredSegue`. These classes are not included in the SwiftMessages to avoid cluttering the Segue Type
dialog by default. Therefore, SwiftMessagesSegueExtras must be explicitly added to the project
(see the View Controller readme).
`SwiftMessagesSegue` can be used without an associated storyboard or segue by doing the following in
the presenting view controller.
let destinationVC = ... // make a reference to a destination view controller
let segue = SwiftMessagesSegue(identifier: nil, source: self, destination: destinationVC)
... // do any configuration here
segue.perform()
To dismiss, call the UIKit API on the presenting view controller:
dismiss(animated: true, completion: nil)
It is not necessary to retain `segue` because it retains itself until dismissal. However, you can
retain it if you plan to `perform()` more than once.
+ note: Some additional details:
1. Your view controller's view will be embedded in a `SwiftMessages.BaseView` in order to
utilize some SwiftMessages features. This view can be accessed and configured via the
`SwiftMessagesSegue.messageView` property. For example, you may configure a default drop
shadow by calling `segue.messageView.configureDropShadow()`.
2. SwiftMessages relies on a view's `intrinsicContentSize` to determine the height of a message.
However, some view controllers' views does not define a good `intrinsicContentSize`
(`UINavigationController` is a common example). For these cases, there are a couple of ways
to specify the preferred height. First, you may set the `preferredContentSize` on the destination
view controller (available as "Use Preferred Explicit Size" in IB's attribute inspector). Second,
you may set `SwiftMessagesSegue.messageView.backgroundHeight`.
See the "View Controllers" selection in the Demo app for examples.
*/
open class SwiftMessagesSegue: UIStoryboardSegue {
/**
Specifies one of the pre-defined layouts, mirroring a subset of `MessageView.Layout`.
*/
public enum Layout {
/// The standard message view layout on top.
case topMessage
/// The standard message view layout on bottom.
case bottomMessage
/// A floating card-style view with rounded corners on top
case topCard
/// A floating tab-style view with rounded corners on bottom
case topTab
/// A floating card-style view with rounded corners on bottom
case bottomCard
/// A floating tab-style view with rounded corners on top
case bottomTab
/// A floating card-style view typically used with `.center` presentation style.
case centered
}
/**
Specifies how the view controller's view is installed into the
containing message view.
*/
public enum Containment {
/**
The view controller's view is installed for edge-to-edge display, extending into the safe areas
to the device edges. This is done by calling `messageView.installContentView(:insets:)`
See that method's documentation for additional details.
*/
case content
/**
The view controller's view is installed for card-style layouts, inset from the margins
and avoiding safe areas. This is done by calling `messageView.installBackgroundView(:insets:)`.
See that method's documentation for details.
*/
case background
/**
The view controller's view is installed for tab-style layouts, inset from the side margins, but extending
to the device edge on the top or bottom. This is done by calling `messageView.installBackgroundVerticalView(:insets:)`.
See that method's documentation for details.
*/
case backgroundVertical
}
/**
The view that is passed to `SwiftMessages.show(config:view:)` during presentation.
The view controller's view is installed into `containerView`, which is itself installed
into `messageView`. `SwiftMessagesSegue` does this installation automatically based on the
value of the `containment` property. `BaseView` is the parent of `MessageView` and provides a
number of configuration options that you may use. For example, you may configure a default drop
shadow by calling `messageView.configureDropShadow()`.
*/
public var messageView = BaseView()
/**
The view controller's view is embedded in `containerView` before being installed into
`messageView`. This view provides configurable squircle (round) corners (see the parent
class `CornerRoundingView`).
*/
public var containerView = ViewControllerContainerView()
/**
Specifies how the view controller's view is installed into the
containing message view. See `Containment` for details.
*/
public var containment: Containment = .content
/// The presentation style to use. See the SwiftMessages.PresentationStyle for details.
public var presentationStyle: SwiftMessages.PresentationStyle {
get { return messenger.defaultConfig.presentationStyle }
set { messenger.defaultConfig.presentationStyle = newValue }
}
/// The dim mode to use. See the SwiftMessages.DimMode for details.
public var dimMode: SwiftMessages.DimMode {
get { return messenger.defaultConfig.dimMode}
set { messenger.defaultConfig.dimMode = newValue }
}
/// Specifies whether or not the interactive pan-to-hide gesture is enabled
/// on the message view. The default value is `true`, but may not be appropriate
/// for view controllers that use swipe or pan gestures.
public var interactiveHide: Bool {
get { return messenger.defaultConfig.interactiveHide }
set { messenger.defaultConfig.interactiveHide = newValue }
}
private var messenger = SwiftMessages()
private var selfRetainer: SwiftMessagesSegue? = nil
private lazy var hider = { return Hider(segue: self) }()
private lazy var presenter = {
return Presenter(config: messenger.defaultConfig, view: messageView, delegate: messenger)
}()
override open func perform() {
selfRetainer = self
destination.modalPresentationStyle = .custom
destination.transitioningDelegate = self
source.present(destination, animated: true, completion: nil)
}
override public init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: destination)
dimMode = .gray(interactive: true)
messenger.defaultConfig.duration = .forever
}
fileprivate let safeAreaWorkaroundViewController = UIViewController()
}
extension SwiftMessagesSegue {
/// A convenience method for configuring some pre-defined layouts that mirror a subset of `MessageView.Layout`.
public func configure(layout: Layout) {
messageView.bounceAnimationOffset = 0
messageView.statusBarOffset = 0
messageView.safeAreaTopOffset = 0
messageView.safeAreaBottomOffset = 0
containment = .content
containerView.cornerRadius = 0
containerView.roundsLeadingCorners = false
messageView.configureDropShadow()
switch layout {
case .topMessage:
messageView.layoutMarginAdditions = UIEdgeInsetsMake(20, 20, 20, 20)
messageView.collapseLayoutMarginAdditions = false
let animation = TopBottomAnimation(style: .top)
animation.springDamping = 1
presentationStyle = .custom(animator: animation)
case .bottomMessage:
messageView.layoutMarginAdditions = UIEdgeInsetsMake(20, 20, 20, 20)
messageView.collapseLayoutMarginAdditions = false
let animation = TopBottomAnimation(style: .bottom)
animation.springDamping = 1
presentationStyle = .custom(animator: animation)
case .topCard:
containment = .background
messageView.layoutMarginAdditions = UIEdgeInsetsMake(10, 10, 10, 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
presentationStyle = .top
case .bottomCard:
containment = .background
messageView.layoutMarginAdditions = UIEdgeInsetsMake(10, 10, 10, 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
presentationStyle = .bottom
case .topTab:
containment = .backgroundVertical
messageView.layoutMarginAdditions = UIEdgeInsetsMake(20, 10, 20, 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
containerView.roundsLeadingCorners = true
let animation = TopBottomAnimation(style: .top)
animation.springDamping = 1
presentationStyle = .custom(animator: animation)
case .bottomTab:
containment = .backgroundVertical
messageView.layoutMarginAdditions = UIEdgeInsetsMake(20, 10, 20, 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
containerView.roundsLeadingCorners = true
let animation = TopBottomAnimation(style: .bottom)
animation.springDamping = 1
presentationStyle = .custom(animator: animation)
case .centered:
containment = .backgroundVertical
messageView.layoutMarginAdditions = UIEdgeInsetsMake(20, 10, 20, 10)
messageView.collapseLayoutMarginAdditions = true
containerView.cornerRadius = 15
presentationStyle = .center
}
}
}
extension SwiftMessagesSegue: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let shower = Shower(segue: self)
messenger.defaultConfig.eventListeners.append { [unowned self] in
switch $0 {
case .didShow:
shower.completeTransition?(true)
case .didHide:
if let completeTransition = self.hider.completeTransition {
completeTransition(true)
} else {
// Case where message is interinally hidden by SwiftMessages, such as with a
// dismiss gesture, rather than by view controller dismissal.
source.dismiss(animated: false, completion: nil)
}
self.selfRetainer = nil
default: break
}
}
return shower
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return hider
}
}
extension SwiftMessagesSegue {
private class Shower: NSObject, UIViewControllerAnimatedTransitioning {
fileprivate private(set) var completeTransition: ((Bool) -> Void)?
private weak var segue: SwiftMessagesSegue?
fileprivate init(segue: SwiftMessagesSegue) {
self.segue = segue
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return segue?.presenter.animator.showDuration ?? 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let segue = segue,
let toView = transitionContext.view(forKey: .to) else {
transitionContext.completeTransition(false)
return
}
if #available(iOS 12, *) {}
else if #available(iOS 11.0, *) {
// This works around a bug in iOS 11 where the safe area of `messageView` (
// and all ancestor views) is not set except on iPhone X. By assigning `messageView`
// to a view controller, its safe area is set consistently. This bug has been resolved as
// of Xcode 10 beta 2.
segue.safeAreaWorkaroundViewController.view = segue.presenter.maskingView
}
completeTransition = transitionContext.completeTransition
let transitionContainer = transitionContext.containerView
// Setup the layout of the `toView`
do {
toView.translatesAutoresizingMaskIntoConstraints = false
segue.containerView.addSubview(toView)
toView.topAnchor.constraint(equalTo: segue.containerView.topAnchor).isActive = true
toView.bottomAnchor.constraint(equalTo: segue.containerView.bottomAnchor).isActive = true
toView.leftAnchor.constraint(equalTo: segue.containerView.leftAnchor).isActive = true
toView.rightAnchor.constraint(equalTo: segue.containerView.rightAnchor).isActive = true
}
// Install the `toView` into the message view.
switch segue.containment {
case .content:
segue.messageView.installContentView(segue.containerView)
case .background:
segue.messageView.installBackgroundView(segue.containerView)
case .backgroundVertical:
segue.messageView.installBackgroundVerticalView(segue.containerView)
}
segue.containerView.viewController = transitionContext.viewController(forKey: .to)
segue.presenter.config.presentationContext = .view(transitionContainer)
segue.messenger.show(presenter: segue.presenter)
}
}
}
extension SwiftMessagesSegue {
private class Hider: NSObject, UIViewControllerAnimatedTransitioning {
fileprivate private(set) var completeTransition: ((Bool) -> Void)?
private weak var segue: SwiftMessagesSegue?
fileprivate init(segue: SwiftMessagesSegue) {
self.segue = segue
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return segue?.presenter.animator.hideDuration ?? 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let messenger = segue?.messenger else {
transitionContext.completeTransition(false)
return
}
completeTransition = transitionContext.completeTransition
messenger.hide()
}
}
}