Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save xuao575/f76b3e01f1b106d1ab0fdf674b24c2c1 to your computer and use it in GitHub Desktop.
Save xuao575/f76b3e01f1b106d1ab0fdf674b24c2c1 to your computer and use it in GitHub Desktop.
Control Interactive Dismissal of Navigation Zoom Transition SwiftUI
// AllowedNavigationDismissalGestures.swift
// PaperPuppy
// Created by Ao XU on 5/19/25.
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]
/// 左缘滑 + 缩放边缘滑动
public static let edgePanGesturesOnly: AllowedNavigationDismissalGestures = [.swipeToGoBack, .zoomEdgePanToDismiss]
/// 所有缩放过渡手势(缩放边缘、下拉、捏合)
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))
}
/// 新增:通过 Bool 动态开关。当 isEnabled == false 时,相当于 .none
func navigationAllowDismissalGestures(_ gestures: AllowedNavigationDismissalGestures = .all,
isEnabled: Bool) -> some View {
let allowed = isEnabled ? gestures : .none
return modifier(NavigationAllowedDismissalGesturesModifier(allowedDismissalGestures: allowed))
}
}
// MARK: - NavigationAllowedDismissalGesturesModifier
private struct NavigationAllowedDismissalGesturesModifier: ViewModifier {
var allowedDismissalGestures: AllowedNavigationDismissalGestures
func body(content: Content) -> some View {
content
.background(
NavigationDismissalGestureUpdater(allowedDismissalGestures: allowedDismissalGestures)
.frame(width: 0, height: 0)
)
}
}
// MARK: - NavigationDismissalGestureUpdater
private struct NavigationDismissalGestureUpdater: UIViewControllerRepresentable {
@State private var viewMountRetryCount = 0
var allowedDismissalGestures: AllowedNavigationDismissalGestures
func makeUIViewController(context: Context) -> UIViewController { .init() }
func updateUIViewController(_ vc: UIViewController, context: Context) {
Task { @MainActor in
guard
let parent = vc.parent,
let nav = parent.navigationController
else {
if viewMountRetryCount < Constants.maxRetryCountForNavigationHeirarchy {
viewMountRetryCount += 1
try await Task.sleep(for: .milliseconds(100))
return updateUIViewController(vc, context: context)
}
return
}
guard nav.topViewController == parent else { return }
// 系统左缘滑动
nav.interactivePopGestureRecognizer?.isEnabled = allowedDismissalGestures.contains(.swipeToGoBack)
// 私有的缩放手势
let recognizers = parent.view.gestureRecognizers ?? []
for gr in recognizers {
switch String(describing: type(of: gr)) {
case Constants.zoomEdgePanToDismissClassType:
gr.isEnabled = allowedDismissalGestures.contains(.zoomEdgePanToDismiss)
case Constants.zoomSwipeDownToDismissClassType:
gr.isEnabled = allowedDismissalGestures.contains(.zoomSwipeDownToDismiss)
case Constants.zoomPinchToDismissClassType:
gr.isEnabled = allowedDismissalGestures.contains(.zoomPinchToDismiss)
default:
continue
}
}
}
}
static func dismantleUIViewController(_ vc: UIViewController, coordinator: ()) {
vc.parent?.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
(vc.parent?.view.gestureRecognizers ?? []).forEach { gr in
if Constants.navigationZoomGestureTypeClasses.contains(String(describing: type(of: gr))) {
gr.isEnabled = true
}
}
}
private enum Constants {
static let maxRetryCountForNavigationHeirarchy = 2
static let zoomEdgePanToDismissClassType = "_UIParallaxTransitionPanGestureRecognizer"
static let zoomSwipeDownToDismissClassType = "_UISwipeDownGestureRecognizer"
static let zoomPinchToDismissClassType = "_UITransformGestureRecognizer"
static let navigationZoomGestureTypeClasses: Set<String> = [
zoomEdgePanToDismissClassType,
zoomSwipeDownToDismissClassType,
zoomPinchToDismissClassType,
]
}
}
@xuao575
Copy link
Author

xuao575 commented May 18, 2025

.navigationAllowDismissalGestures(.edgePanGesturesOnly, isEnabled: allowSwipeBack)

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