|
// 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, |
|
] |
|
} |
|
} |
.navigationAllowDismissalGestures(.edgePanGesturesOnly, isEnabled: allowSwipeBack)