Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save DabbyNdubisi/c4045a0231435c22be887cb6d9109507 to your computer and use it in GitHub Desktop.
Save DabbyNdubisi/c4045a0231435c22be887cb6d9109507 to your computer and use it in GitHub Desktop.
Control Interactive Dismissal of Navigation Zoom Transition SwiftUI
import SwiftUI
import UIKit
import Foundation
// MARK: - AllowedNavigationDismissalGestures
public struct AllowedNavigationDismissalGestures: OptionSet, Sendable {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let none: AllowedNavigationDismissalGestures = []
/// Default behaviour
public static let all: AllowedNavigationDismissalGestures = [.swipeToGoBack, .zoomTransitionGesturesOnly]
/// Includes both regular left-right swipe to go back and edge-pan for zoom transition dismisall
public static let edgePanGesturesOnly: AllowedNavigationDismissalGestures = [.swipeToGoBack, .zoomEdgePanToDismiss]
/// Includes all zoom transition gestures: edge-pan, swipe-down, pinch
public static let zoomTransitionGesturesOnly: AllowedNavigationDismissalGestures = [.zoomEdgePanToDismiss, .zoomSwipeDownToDismiss, .zoomPinchToDismiss]
public static let swipeToGoBack = AllowedNavigationDismissalGestures(rawValue: 1 << 0)
public static let zoomEdgePanToDismiss = AllowedNavigationDismissalGestures(rawValue: 1 << 1)
public static let zoomSwipeDownToDismiss = AllowedNavigationDismissalGestures(rawValue: 1 << 2)
public static let zoomPinchToDismiss = AllowedNavigationDismissalGestures(rawValue: 1 << 3)
}
public extension View {
func navigationAllowDismissalGestures(_ gestures: AllowedNavigationDismissalGestures = .all) -> some View {
modifier(NavigationAllowedDismissalGesturesModifier(allowedDismissalGestures: gestures))
}
}
// MARK: - NavigationAllowedDismissalGesturesModifier
private struct NavigationAllowedDismissalGesturesModifier: ViewModifier {
var allowedDismissalGestures: AllowedNavigationDismissalGestures
func body(content: Content) -> some View {
content
.background(
NavigationDismissalGestureUpdater(allowedDismissalGestures: allowedDismissalGestures)
.frame(width: .zero, height: .zero)
)
}
}
// MARK: - NavigationDismissalGestureUpdater
private struct NavigationDismissalGestureUpdater: UIViewControllerRepresentable {
@State private var viewMountRetryCount = 0
var allowedDismissalGestures: AllowedNavigationDismissalGestures
func makeUIViewController(context: Context) -> UIViewController { .init() }
func updateUIViewController(_ viewController: UIViewController, context: Context) {
Task { @MainActor in
guard
let parentVC = viewController.parent,
let navigationController = parentVC.navigationController
else {
// updateUIViewController could get called a bit too early
// before the view heirarchy has been fully setup
if viewMountRetryCount < Constants.maxRetryCountForNavigationHeirarchy {
viewMountRetryCount += 1
try await Task.sleep(for: .milliseconds(100))
return updateUIViewController(viewController, context: context)
} else {
// unable to find navigation controller
return
}
}
guard navigationController.topViewController == parentVC else {
return
}
navigationController.interactivePopGestureRecognizer?.isEnabled = allowedDismissalGestures.contains(.swipeToGoBack)
let viewLevelGestures = parentVC.view.gestureRecognizers ?? []
for gesture in viewLevelGestures {
switch String(describing: type(of: gesture)) {
case Constants.zoomEdgePanToDismissClassType:
gesture.isEnabled = allowedDismissalGestures.contains(.zoomEdgePanToDismiss)
case Constants.zoomSwipeDownToDismissClassType:
gesture.isEnabled = allowedDismissalGestures.contains(.zoomSwipeDownToDismiss)
case Constants.zoomPinchToDismissClassType:
gesture.isEnabled = allowedDismissalGestures.contains(.zoomPinchToDismiss)
default:
continue
}
}
}
}
static func dismantleUIViewController(_ viewController: UIViewController, coordinator: Coordinator) {
viewController.parent?.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
(viewController.parent?.view.gestureRecognizers ?? []).forEach({ gesture in
if Constants.navigationZoomGestureTypeClasses.contains(String(describing: type(of: gesture))) {
gesture.isEnabled = true
}
})
}
// MARK: Private
private enum Constants {
static let maxRetryCountForNavigationHeirarchy = 2
// These are private Navigation related UIKit gesture recognizers that we want to disable
// when the swipe to go back is disabled.
static let zoomEdgePanToDismissClassType: String = "_UIParallaxTransitionPanGestureRecognizer" // Edge pan zoom transition dismissal gesture
static let zoomSwipeDownToDismissClassType: String = "_UISwipeDownGestureRecognizer" // Swipe down to dismiss gesture
static let zoomPinchToDismissClassType: String = "_UITransformGestureRecognizer" // Pinch to dismiss gesture
static let navigationZoomGestureTypeClasses: Set<String> = [
zoomEdgePanToDismissClassType,
zoomSwipeDownToDismissClassType,
zoomPinchToDismissClassType,
]
}
}
@gongzhang
Copy link

Works like magic. Saved me a day. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment