Skip to content

Commit ee0a4c6

Browse files
committed
Add dismissaibleIn and non-animated disappear
1 parent 618ac0d commit ee0a4c6

File tree

5 files changed

+99
-31
lines changed

5 files changed

+99
-31
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,11 @@ scroll parameters:
275275
`headerView` - a view on top which won't be a part of the scroll (if you need one)
276276

277277
`position` - topLeading, top, topTrailing, leading, center, trailing, bottomLeading, bottom, bottomTrailing
278-
`appearFrom` - `topSlide, bottomSlide, leftSlide, rightSlide, centerScale`: determines the direction of appearing animation. If left empty it copies `position` parameter: so appears from .top edge, if `position` is set to .top
278+
`appearFrom` - `topSlide, bottomSlide, leftSlide, rightSlide, centerScale, none`: determines the direction of appearing animation. If left empty it copies `position` parameter: so appears from .top edge, if `position` is set to .top. `.none` means no animation
279279
`disappearTo` - same as `appearFrom`, but for disappearing animation. If left empty it copies `appearFrom`.
280280
`animation` - custom animation for popup sliding onto screen
281281
`autohideIn` - time after which popup should disappear
282+
`dismissibleIn(Double?, Binding<Bool>?)` - only allow dismiss after this time passes (forbids closeOnTap, closeOnTapOutside, and drag). Pass a boolean binding if you'd like to track current status
282283
`dragToDismiss` - true by default: enable/disable drag to dismiss (upwards for .top popup types, downwards for .bottom and default type)
283284
`closeOnTap` - true by default: enable/disable closing on tap on popup
284285
`closeOnTapOutside` - false by default: enable/disable closing on tap on outside of popup

Sources/PopupView/FullscreenPopup.swift

+47-8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
2929
/// If nil - never hides on its own
3030
var autohideIn: Double?
3131

32+
/// Only allow dismiss by any means after this time passes
33+
var dismissibleIn: Double?
34+
35+
/// Becomes true when `dismissibleIn` times finishes
36+
/// Makes no sense if `dismissibleIn` is nil
37+
var dismissEnabled: Binding<Bool>
38+
3239
/// Should close on tap outside - default is `false`
3340
var closeOnTapOutside: Bool
3441

@@ -79,7 +86,10 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
7986
private var itemRef: ClassReference<Binding<Item?>>?
8087

8188
/// holder for autohiding dispatch work (to be able to cancel it when needed)
82-
@State private var dispatchWorkHolder = DispatchWorkHolder()
89+
@State private var autohidingWorkHolder = DispatchWorkHolder()
90+
91+
/// holder for `dismissibleIn` dispatch work (to be able to cancel it when needed)
92+
@State private var dismissibleInWorkHolder = DispatchWorkHolder()
8393

8494
// MARK: - Autohide With Dragging
8595
/// If user "grabbed" the popup to drag it around, put off the autohiding until he lifts his finger up
@@ -90,6 +100,10 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
90100
/// if autohide time was set up, shows that timer has come to an end already
91101
@State private var timeToHide = false
92102

103+
// MARK: - dismissibleIn
104+
105+
private var dismissEnabledRef: ClassReference<Binding<Bool>>?
106+
93107
// MARK: - Internal
94108

95109
/// Set dismiss source to pass to dismiss callback
@@ -111,6 +125,8 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
111125

112126
self.params = params
113127
self.autohideIn = params.autohideIn
128+
self.dismissibleIn = params.dismissibleIn
129+
self.dismissEnabled = params.dismissEnabled
114130
self.closeOnTapOutside = params.closeOnTapOutside
115131
self.backgroundColor = params.backgroundColor
116132
self.backgroundView = params.backgroundView
@@ -127,6 +143,7 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
127143

128144
self.isPresentedRef = ClassReference(self.$isPresented)
129145
self.itemRef = ClassReference(self.$item)
146+
self.dismissEnabledRef = ClassReference(self.dismissEnabled)
130147
}
131148

132149
public func body(content: Content) -> some View {
@@ -217,7 +234,8 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
217234
dismissSource: $dismissSource,
218235
backgroundColor: backgroundColor,
219236
backgroundView: backgroundView,
220-
closeOnTapOutside: closeOnTapOutside
237+
closeOnTapOutside: closeOnTapOutside,
238+
dismissEnabled: dismissEnabled
221239
)
222240
.modifier(getModifier())
223241
}
@@ -236,7 +254,6 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
236254
Popup(
237255
params: params,
238256
view: viewForItem != nil ? viewForItem! : view,
239-
popupPresented: popupPresented,
240257
shouldShowContent: $shouldShowContent,
241258
showContent: showContent,
242259
isDragging: $isDragging,
@@ -251,6 +268,7 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
251268
}
252269
}
253270
setupAutohide()
271+
setupdismissibleIn()
254272
}
255273
},
256274
dismissCallback: { source in
@@ -270,7 +288,8 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
270288
} else {
271289
closingIsInProcess = true
272290
userWillDismissCallback(dismissSource ?? .binding)
273-
dispatchWorkHolder.work?.cancel()
291+
autohidingWorkHolder.work?.cancel()
292+
dismissibleInWorkHolder.work?.cancel()
274293
shouldShowContent = false // this will cause currentOffset change thus triggering the sliding hiding animation
275294
animatableOpacity = 0
276295
// do the rest once the animation is finished (see onAnimationCompleted())
@@ -289,6 +308,9 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
289308
}
290309
showContent = false // unload popup body after hiding animation is done
291310
tempItemView = nil
311+
if dismissibleIn != nil {
312+
dismissEnabled.wrappedValue = false
313+
}
292314
performWithDelay(0.01) {
293315
showSheet = false
294316
}
@@ -302,27 +324,44 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
302324
func setupAutohide() {
303325
// if needed, dispatch autohide and cancel previous one
304326
if let autohideIn = autohideIn {
305-
dispatchWorkHolder.work?.cancel()
327+
autohidingWorkHolder.work?.cancel()
306328

307329
// Weak reference to avoid the work item capturing the struct,
308330
// which would create a retain cycle with the work holder itself.
309331

310-
dispatchWorkHolder.work = DispatchWorkItem(block: { [weak isPresentedRef, weak itemRef] in
332+
autohidingWorkHolder.work = DispatchWorkItem(block: { [weak isPresentedRef, weak itemRef] in
311333
if isDragging {
312334
timeToHide = true // raise this flag to hide the popup once the drag is over
313335
return
314336
}
315337
dismissSource = .autohide
316338
isPresentedRef?.value.wrappedValue = false
317339
itemRef?.value.wrappedValue = nil
318-
dispatchWorkHolder.work = nil
340+
autohidingWorkHolder.work = nil
319341
})
320-
if popupPresented, let work = dispatchWorkHolder.work {
342+
if popupPresented, let work = autohidingWorkHolder.work {
321343
DispatchQueue.main.asyncAfter(deadline: .now() + autohideIn, execute: work)
322344
}
323345
}
324346
}
325347

348+
func setupdismissibleIn() {
349+
if let dismissibleIn = dismissibleIn {
350+
dismissibleInWorkHolder.work?.cancel()
351+
352+
// Weak reference to avoid the work item capturing the struct,
353+
// which would create a retain cycle with the work holder itself.
354+
355+
dismissibleInWorkHolder.work = DispatchWorkItem(block: { [weak dismissEnabledRef] in
356+
dismissEnabledRef?.value.wrappedValue = true
357+
dismissibleInWorkHolder.work = nil
358+
})
359+
if popupPresented, let work = dismissibleInWorkHolder.work {
360+
DispatchQueue.main.asyncAfter(deadline: .now() + dismissibleIn, execute: work)
361+
}
362+
}
363+
}
364+
326365
func performWithDelay(_ delay: Double, block: @escaping ()->()) {
327366
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
328367
block()

Sources/PopupView/PopupBackgroundView.swift

+6-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct PopupBackgroundView<Item: Equatable>: View {
2121
var backgroundColor: Color
2222
var backgroundView: AnyView?
2323
var closeOnTapOutside: Bool
24+
var dismissEnabled: Binding<Bool>
2425

2526
var body: some View {
2627
Group {
@@ -35,9 +36,11 @@ struct PopupBackgroundView<Item: Equatable>: View {
3536
view.contentShape(Rectangle())
3637
}
3738
.addTapIfNotTV(if: closeOnTapOutside) {
38-
dismissSource = .tapOutside
39-
isPresented = false
40-
item = nil
39+
if dismissEnabled.wrappedValue {
40+
dismissSource = .tapOutside
41+
isPresented = false
42+
item = nil
43+
}
4144
}
4245
.edgesIgnoringSafeArea(.all)
4346
.animation(.linear(duration: 0.2), value: animatableOpacity)

Sources/PopupView/PopupView.swift

+27-19
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ public struct Popup<PopupContent: View>: ViewModifier {
1515

1616
init(params: Popup<PopupContent>.PopupParameters,
1717
view: @escaping () -> PopupContent,
18-
popupPresented: Bool,
1918
shouldShowContent: Binding<Bool>,
2019
showContent: Bool,
2120
isDragging: Binding<Bool>,
@@ -35,11 +34,11 @@ public struct Popup<PopupContent: View>: ViewModifier {
3534
self.animation = params.animation
3635
self.dragToDismiss = params.dragToDismiss
3736
self.dragToDismissDistance = params.dragToDismissDistance
37+
self.dismissEnabled = params.dismissEnabled
3838
self.closeOnTap = params.closeOnTap
3939

4040
self.view = view
4141

42-
self.popupPresented = popupPresented
4342
self.shouldShowContent = shouldShowContent
4443
self.showContent = showContent
4544
self._isDragging = isDragging
@@ -85,6 +84,10 @@ public struct Popup<PopupContent: View>: ViewModifier {
8584

8685
var animation: Animation
8786

87+
/// Becomes true when `dismissibleIn` times finishes
88+
/// Makes no sense if `dismissibleIn` is nil
89+
var dismissEnabled: Binding<Bool>
90+
8891
/// Should close on tap - default is `true`
8992
var closeOnTap: Bool
9093

@@ -94,9 +97,6 @@ public struct Popup<PopupContent: View>: ViewModifier {
9497
/// Minimum distance to drag to dismiss
9598
var dragToDismissDistance: CGFloat?
9699

97-
/// Variable showing changes in isPresented/item, used here to determine direction of animation (showing or hiding)
98-
var popupPresented: Bool
99-
100100
/// Trigger popup showing/hiding animations and...
101101
var shouldShowContent: Binding<Bool>
102102

@@ -151,7 +151,10 @@ public struct Popup<PopupContent: View>: ViewModifier {
151151
@State private var scrollViewOffset: CGSize = .zero
152152

153153
/// Height of scrollView content that will be displayed on the screen
154-
@State var scrollViewContentHeight = 0.0
154+
@State private var scrollViewContentHeight = 0.0
155+
156+
/// Track ScrollView's frame to check if it's ready
157+
@State private var scrollViewRect: CGRect = .zero
155158

156159
// MARK: - Position calculations
157160

@@ -223,7 +226,7 @@ public struct Popup<PopupContent: View>: ViewModifier {
223226
}
224227

225228
// appearing animation
226-
if popupPresented {
229+
if shouldShowContent.wrappedValue {
227230
return hiddenOffset(calculatedAppearFrom)
228231
}
229232
// hiding animation
@@ -242,7 +245,7 @@ public struct Popup<PopupContent: View>: ViewModifier {
242245
return CGPoint(x: -screenWidth, y: displayedOffsetY)
243246
case .rightSlide:
244247
return CGPoint(x: screenWidth, y: displayedOffsetY)
245-
case .centerScale:
248+
case .centerScale, .none:
246249
return CGPoint(x: displayedOffsetX, y: displayedOffsetY)
247250
}
248251
}
@@ -261,10 +264,10 @@ public struct Popup<PopupContent: View>: ViewModifier {
261264

262265
/// The scale when the popup is hidden
263266
private var hiddenScale: CGFloat {
264-
if popupPresented, calculatedAppearFrom == .centerScale {
267+
if shouldShowContent.wrappedValue, calculatedAppearFrom == .centerScale {
265268
return 0
266269
}
267-
else if !popupPresented, calculatedDisappearTo == .centerScale {
270+
else if !shouldShowContent.wrappedValue, calculatedDisappearTo == .centerScale {
268271
return 0
269272
}
270273
return 1
@@ -389,6 +392,7 @@ public struct Popup<PopupContent: View>: ViewModifier {
389392
}
390393
// no heigher than its contents
391394
.frame(maxHeight: scrollViewContentHeight)
395+
.frameGetter($scrollViewRect)
392396
}
393397
.introspect(.scrollView, on: .iOS(.v15, .v16, .v17, .v18)) { scrollView in
394398
configure(scrollView: scrollView)
@@ -412,7 +416,9 @@ public struct Popup<PopupContent: View>: ViewModifier {
412416
VStack {
413417
contentView()
414418
.addTapIfNotTV(if: closeOnTap) {
415-
dismissCallback(.tapInside)
419+
if dismissEnabled.wrappedValue {
420+
dismissCallback(.tapInside)
421+
}
416422
}
417423
.scaleEffect(actualScale) // scale is here to avoid it messing with frameGetter for sheetContentRect
418424
}
@@ -445,15 +451,15 @@ public struct Popup<PopupContent: View>: ViewModifier {
445451
}
446452

447453
.onChange(of: sheetContentRect.size) { sheetContentRect in
454+
// check if scrollView has already calculated its height, otherwise sheetContentRect is already non-zero but yet incorrect
455+
if case .scroll(_) = type, scrollViewRect.height == 0 {
456+
return
457+
}
448458
positionIsCalculatedCallback()
449459
if shouldShowContent.wrappedValue { // already displayed but the size has changed
450460
actualCurrentOffset = targetCurrentOffset
451461
}
452462
}
453-
454-
.onChange(of: actualCurrentOffset) { actualCurrentOffset in
455-
print(actualCurrentOffset)
456-
}
457463
#if os(iOS)
458464
.onOrientationChange(isLandscape: $isLandscape) {
459465
actualCurrentOffset = targetCurrentOffset
@@ -465,7 +471,9 @@ public struct Popup<PopupContent: View>: ViewModifier {
465471
VStack {
466472
contentView()
467473
.addTapIfNotTV(if: closeOnTap) {
468-
dismissCallback(.tapInside)
474+
if dismissEnabled.wrappedValue {
475+
dismissCallback(.tapInside)
476+
}
469477
}
470478
.scaleEffect(actualScale) // scale is here to avoid it messing with frameGetter for sheetContentRect
471479
}
@@ -559,7 +567,7 @@ public struct Popup<PopupContent: View>: ViewModifier {
559567
if dragState.translation.width > 0 {
560568
return CGSize(width: dragState.translation.width, height: 0)
561569
}
562-
case .centerScale:
570+
case .centerScale, .none:
563571
return .zero
564572
}
565573
return .zero
@@ -606,7 +614,7 @@ public struct Popup<PopupContent: View>: ViewModifier {
606614
if drag.translation.width > referenceX {
607615
shouldDismiss = true
608616
}
609-
case .centerScale:
617+
case .centerScale, .none:
610618
break
611619
}
612620

@@ -615,7 +623,7 @@ public struct Popup<PopupContent: View>: ViewModifier {
615623
shouldDismiss = true
616624
}
617625

618-
if shouldDismiss {
626+
if dismissEnabled.wrappedValue, shouldDismiss {
619627
dismissCallback(.drag)
620628
} else {
621629
withAnimation {

Sources/PopupView/PublicAPI.swift

+17
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ extension Popup {
104104
case leftSlide
105105
case rightSlide
106106
case centerScale
107+
case none
107108
}
108109

109110
public struct PopupParameters {
@@ -120,6 +121,13 @@ extension Popup {
120121
/// If nil - never hides on its own
121122
var autohideIn: Double?
122123

124+
/// Only allow dismiss by any means after this time passes
125+
var dismissibleIn: Double?
126+
127+
/// Becomes true when `dismissibleIn` times finishes
128+
/// Makes no sense if `dismissibleIn` is nil
129+
var dismissEnabled: Binding<Bool> = .constant(true)
130+
123131
/// Should allow dismiss by dragging - default is `true`
124132
var dragToDismiss: Bool = true
125133

@@ -189,6 +197,15 @@ extension Popup {
189197
return params
190198
}
191199

200+
public func dismissibleIn(_ dismissibleIn: Double?, _ dismissEnabled: Binding<Bool>?) -> PopupParameters {
201+
var params = self
202+
params.dismissibleIn = dismissibleIn
203+
if let dismissEnabled = dismissEnabled {
204+
params.dismissEnabled = dismissEnabled
205+
}
206+
return params
207+
}
208+
192209
/// Should allow dismiss by dragging - default is `true`
193210
public func dragToDismiss(_ dragToDismiss: Bool) -> PopupParameters {
194211
var params = self

0 commit comments

Comments
 (0)