Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Sources/FlowStack/FlowLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ public struct FlowLink<Label>: View where Label: View {
/// - shadowColor: The shadow color applied to the transitioning destination view. This value should typically match the shadow color of the flow link contents or flow link animation anchor for visual consistency.
/// - shadowOffset: The shadow offset applied to the transitioning destination view. This value should typically match the shadow offset of the flow link contents or flow link animation anchor for visual consistency.
/// - zoomStyle: The zoom style applied to the transitioning destination view
public init(animateFromAnchor: Bool = true, transitionFromSnapshot: Bool = true, retakeSnapshots: Bool = false, cornerRadius: CGFloat = 0, cornerStyle: RoundedCornerStyle = .circular, shadowRadius: CGFloat = 0, shadowColor: Color? = nil, shadowOffset: CGPoint = .zero, zoomStyle: ZoomStyle = .scaleHorizontally) {
/// - swipeUpToDismiss: Whether the destination view should allow swipe up to dismiss
public init(animateFromAnchor: Bool = true, transitionFromSnapshot: Bool = true, retakeSnapshots: Bool = false, cornerRadius: CGFloat = 0, cornerStyle: RoundedCornerStyle = .circular, shadowRadius: CGFloat = 0, shadowColor: Color? = nil, shadowOffset: CGPoint = .zero, zoomStyle: ZoomStyle = .scaleHorizontally, swipeUpToDismiss: Bool = false) {
self.animateFromAnchor = animateFromAnchor
self.transitionFromSnapshot = transitionFromSnapshot
self.retakeSnapshots = retakeSnapshots
Expand All @@ -227,6 +228,7 @@ public struct FlowLink<Label>: View where Label: View {
self.shadowColor = shadowColor
self.shadowOffset = shadowOffset
self.zoomStyle = zoomStyle
self.swipeUpToDismiss = swipeUpToDismiss
}

let animateFromAnchor: Bool
Expand All @@ -242,6 +244,8 @@ public struct FlowLink<Label>: View where Label: View {

let showsSkrim: Bool = true
let zoomStyle: ZoomStyle

let swipeUpToDismiss: Bool
}

public enum Activation { case overlayButton, tapGesture }
Expand Down Expand Up @@ -460,7 +464,8 @@ public struct FlowLink<Label>: View where Label: View {
shadowColor: configuration.shadowColor,
shadowOffset: configuration.shadowOffset,
shouldShowSkrim: configuration.showsSkrim,
shouldScaleHorizontally: configuration.zoomStyle == .scaleHorizontally
shouldScaleHorizontally: configuration.zoomStyle == .scaleHorizontally,
swipeUpToDismiss: configuration.swipeUpToDismiss
)
})
.onPreferenceChange(PathContextKey.self) { value in
Expand Down
2 changes: 2 additions & 0 deletions Sources/FlowStack/FlowPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ struct PathContext: Equatable, Hashable {

var shouldShowSkrim: Bool = true
var shouldScaleHorizontally: Bool = true

var swipeUpToDismiss: Bool = false
}

struct FlowElement: Equatable, Hashable {
Expand Down
2 changes: 1 addition & 1 deletion Sources/FlowStack/FlowTransition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ extension AnyTransition {
let scaleRatio = context.shouldScaleHorizontally ? zoomRect.size.width / proxy.size.width : 1.0

content
.onInteractiveDismissGesture(threshold: 80, isEnabled: !isDisabled, isDismissing: isDismissing, onDismiss: {
.onInteractiveDismissGesture(threshold: 80, isEnabled: !isDisabled, isDismissing: isDismissing, swipeUpToDismiss: context.swipeUpToDismiss, onDismiss: {
dismiss()
isDismissing = true
}, onPan: { offset in
Expand Down
26 changes: 17 additions & 9 deletions Sources/FlowStack/View+InteractiveDismiss.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ struct InteractiveDismissContainer<T: View>: UIViewControllerRepresentable {
var onPan: (CGPoint) -> Void
var isEnabled: Bool
var isDismissing: Bool

var swipeUpToDismiss: Bool

var onDismiss: () -> Void
var onEnded: (Bool) -> Void

Expand All @@ -59,7 +62,7 @@ struct InteractiveDismissContainer<T: View>: UIViewControllerRepresentable {
}

func makeCoordinator() -> InteractiveDismissCoordinator {
InteractiveDismissCoordinator(threshold: threshold, onPan: onPan, isEnabled: isEnabled, isDismissing: isDismissing, onDismiss: onDismiss, onEnded: onEnded)
InteractiveDismissCoordinator(threshold: threshold, onPan: onPan, isEnabled: isEnabled, isDismissing: isDismissing, swipeUpToDismiss: swipeUpToDismiss, onDismiss: onDismiss, onEnded: onEnded)
}
}

Expand Down Expand Up @@ -136,6 +139,9 @@ class InteractiveDismissCoordinator: NSObject, ObservableObject, UIGestureRecogn
handleDismiss()
}
}

var swipeUpToDismiss: Bool

var onDismiss: () -> Void
var onEnded: (Bool) -> Void

Expand Down Expand Up @@ -171,12 +177,13 @@ class InteractiveDismissCoordinator: NSObject, ObservableObject, UIGestureRecogn
}
}

init(threshold: Double, onPan: @escaping (CGPoint) -> Void, isEnabled: Bool, isDismissing: Bool, onDismiss: @escaping () -> Void, onEnded: @escaping (Bool) -> Void) {
init(threshold: Double, onPan: @escaping (CGPoint) -> Void, isEnabled: Bool, isDismissing: Bool, swipeUpToDismiss: Bool, onDismiss: @escaping () -> Void, onEnded: @escaping (Bool) -> Void) {
self.threshold = threshold

self.onPan = onPan
self.isEnabled = isEnabled
self.isDismissing = isDismissing
self.swipeUpToDismiss = swipeUpToDismiss
self.onDismiss = onDismiss
self.onEnded = onEnded

Expand Down Expand Up @@ -215,7 +222,7 @@ class InteractiveDismissCoordinator: NSObject, ObservableObject, UIGestureRecogn
isUpdating = true
onPan(offset)

let shouldDismiss = offset.y > threshold || (offset.x > threshold && isEdge)
let shouldDismiss = offset.y > threshold || (offset.x > threshold && isEdge) || (-offset.y > threshold * 2 && swipeUpToDismiss)
if shouldDismiss != isPastThreshold && shouldDismiss {
impactGenerator.impactOccurred()
}
Expand All @@ -237,9 +244,11 @@ class InteractiveDismissCoordinator: NSObject, ObservableObject, UIGestureRecogn
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGestureRecognizer, let scrollView = scrollView else { return true }

guard panGestureRecognizer.translation(in: scrollView).y > 0 else { return false }

return scrollView.contentOffset.y - 5 <= -scrollView.contentInset.top
if panGestureRecognizer.translation(in: scrollView).y > 0 {
return scrollView.contentOffset.y - 5 <= -scrollView.contentInset.top
} else {
return scrollView.contentOffset.y + UIScreen.main.bounds.height > scrollView.contentSize.height + 20 && swipeUpToDismiss
}
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
Expand All @@ -263,7 +272,6 @@ class InteractiveDismissCoordinator: NSObject, ObservableObject, UIGestureRecogn
otherGestureRecognizer.isEnabled = false
otherGestureRecognizer.isEnabled = true
}

return true
}

Expand All @@ -278,7 +286,7 @@ class InteractiveDismissCoordinator: NSObject, ObservableObject, UIGestureRecogn
}

extension View {
func onInteractiveDismissGesture(threshold: Double = 50, isEnabled: Bool = true, isDismissing: Bool = false, onDismiss: @escaping () -> Void, onPan: @escaping (CGPoint) -> Void = { _ in }, onEnded: @escaping (Bool) -> Void = { _ in }) -> some View {
InteractiveDismissContainer(threshold: threshold, onPan: onPan, isEnabled: isEnabled, isDismissing: isDismissing, onDismiss: onDismiss, onEnded: onEnded, content: self)
func onInteractiveDismissGesture(threshold: Double = 50, isEnabled: Bool = true, isDismissing: Bool = false, swipeUpToDismiss: Bool, onDismiss: @escaping () -> Void, onPan: @escaping (CGPoint) -> Void = { _ in }, onEnded: @escaping (Bool) -> Void = { _ in }) -> some View {
InteractiveDismissContainer(threshold: threshold, onPan: onPan, isEnabled: isEnabled, isDismissing: isDismissing, swipeUpToDismiss: swipeUpToDismiss, onDismiss: onDismiss, onEnded: onEnded, content: self)
}
}