兼容iOS16:修改SwiftUI自定义导航栈的iOS17专属onChange修饰符
解决方案
一、兼容iOS16的onChange修改方案
iOS16的onChange不支持initial: true参数,我们通过条件编译结合onAppear实现等价效果:iOS17+保留带initial参数的onChange,iOS16及以下用onAppear处理初始触发,再用普通onChange监听后续值变化。
修改FullSwipeModifier的核心代码如下:
fileprivate struct FullSwipeModifier: ViewModifier { var isEnabled: Bool var onPopCompleted: (() -> Void)? // 新增:滑动完成回调 @Environment(\.popGestureID) private var gestureID func body(content: Content) -> some View { let popCompletedNotificationName = Notification.Name("\(gestureID ?? "")_popCompleted") content // 兼容iOS16的状态同步逻辑 #if os(iOS) && swift(>=5.9) // iOS17+ .onChange(of: isEnabled, initial: true) { _, newValue in guard let gestureID = gestureID else { return } NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: ["status": newValue]) } #else // iOS16及以下 .onAppear { guard let gestureID = gestureID else { return } NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: ["status": isEnabled]) } .onChange(of: isEnabled) { newValue in guard let gestureID = gestureID else { return } NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: ["status": newValue]) } #endif .onDisappear(perform: { guard let gestureID = gestureID else { return } NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: ["status": false]) }) // 监听滑动返回完成通知 .onReceive(NotificationCenter.default.publisher(for: popCompletedNotificationName)) { _ in onPopCompleted?() } } }
二、滑动返回结束后执行函数的方案
我们通过监听自定义滑动手势的结束状态,结合导航栈的实际变化,在视图真正弹出后发送通知,再在SwiftUI视图中接收通知执行逻辑,完全替代onDisappear。
1. 给手势添加状态监听
在UINavigationController扩展中新增手势状态处理逻辑,仅当手势结束且导航栈确实弹出视图时发送通知:
fileprivate extension UINavigationController { func addFullSwipeGesture(_ gesture: UIPanGestureRecognizer) { guard let gestureSelector = interactivePopGestureRecognizer?.value(forKey: "targets") else { return } gesture.setValue(gestureSelector, forKey: "targets") // 添加手势状态监听 gesture.addTarget(self, action: #selector(handleFullSwipeGesture(_:))) view.addGestureRecognizer(gesture) } @objc private func handleFullSwipeGesture(_ gesture: UIPanGestureRecognizer) { // 仅在手势结束时检查 guard gesture.state == .ended else { return } let originalVCCount = viewControllers.count // 延迟执行,确保pop动画完成、导航栈更新 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { if self.viewControllers.count < originalVCCount { // 发送滑动完成通知 NotificationCenter.default.post(name: .init("\(gesture.name ?? "")_popCompleted"), object: nil) } } } }
2. 扩展修饰器支持回调
让enableFullSwipePop方法支持传入滑动完成的自定义逻辑:
extension View { @ViewBuilder func enableFullSwipePop(_ isEnabled: Bool, onPopCompleted: (() -> Void)? = nil) -> some View { self .modifier(FullSwipeModifier(isEnabled: isEnabled, onPopCompleted: onPopCompleted)) } }
3. 使用示例
在目标视图中直接传入回调即可:
Text("hello") .enableFullSwipePop(isEnabled) { // 这里写滑动返回结束后的逻辑 print("滑动返回完成,执行自定义操作") }
修改后的完整代码
import SwiftUI struct ContentView: View { @State private var isEnabled: Bool = false var body: some View { FullSwipeNavigationStack { NavigationLink("Leading Swipe View") { Text("hello") .enableFullSwipePop(isEnabled) { print("滑动返回完成!") } } } } } struct FullSwipeNavigationStack<Content: View>: View { @ViewBuilder var content: Content @State private var customGesture: UIPanGestureRecognizer = { let gesture = UIPanGestureRecognizer() gesture.name = UUID().uuidString gesture.isEnabled = false return gesture }() var body: some View { NavigationStack { content .background { AttachGestureView(gesture: $customGesture) } } .environment(\.popGestureID, customGesture.name) .onReceive(NotificationCenter.default.publisher(for: .init(customGesture.name ?? "")), perform: { info in if let userInfo = info.userInfo, let status = userInfo["status"] as? Bool { customGesture.isEnabled = status } }) } } extension View { @ViewBuilder func enableFullSwipePop(_ isEnabled: Bool, onPopCompleted: (() -> Void)? = nil) -> some View { self .modifier(FullSwipeModifier(isEnabled: isEnabled, onPopCompleted: onPopCompleted)) } } fileprivate struct PopNotificationID: EnvironmentKey { static var defaultValue: String? } fileprivate extension EnvironmentValues { var popGestureID: String? { get { self[PopNotificationID.self] } set { self[PopNotificationID.self] = newValue } } } fileprivate struct FullSwipeModifier: ViewModifier { var isEnabled: Bool var onPopCompleted: (() -> Void)? @Environment(\.popGestureID) private var gestureID func body(content: Content) -> some View { let popCompletedNotificationName = Notification.Name("\(gestureID ?? "")_popCompleted") content #if os(iOS) && swift(>=5.9) .onChange(of: isEnabled, initial: true) { _, newValue in guard let gestureID = gestureID else { return } NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: ["status": newValue]) } #else .onAppear { guard let gestureID = gestureID else { return } NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: ["status": isEnabled]) } .onChange(of: isEnabled) { newValue in guard let gestureID = gestureID else { return } NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: ["status": newValue]) } #endif .onDisappear(perform: { guard let gestureID = gestureID else { return } NotificationCenter.default.post(name: .init(gestureID), object: nil, userInfo: ["status": false]) }) .onReceive(NotificationCenter.default.publisher(for: popCompletedNotificationName)) { _ in onPopCompleted?() } } } fileprivate struct AttachGestureView: UIViewRepresentable { @Binding var gesture: UIPanGestureRecognizer func makeUIView(context: Context) -> UIView { UIView() } func updateUIView(_ uiView: UIView, context: Context) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { if let parentViewController = uiView.parentViewController, let navigationController = parentViewController.navigationController { if navigationController.view.gestureRecognizers?.contains(where: { $0.name == gesture.name }) == false { navigationController.addFullSwipeGesture(gesture) } } } } } fileprivate extension UINavigationController { func addFullSwipeGesture(_ gesture: UIPanGestureRecognizer) { guard let gestureSelector = interactivePopGestureRecognizer?.value(forKey: "targets") else { return } gesture.setValue(gestureSelector, forKey: "targets") gesture.addTarget(self, action: #selector(handleFullSwipeGesture(_:))) view.addGestureRecognizer(gesture) } @objc private func handleFullSwipeGesture(_ gesture: UIPanGestureRecognizer) { guard gesture.state == .ended else { return } let originalVCCount = viewControllers.count DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { if self.viewControllers.count < originalVCCount { NotificationCenter.default.post(name: .init("\(gesture.name ?? "")_popCompleted"), object: nil) } } } } fileprivate extension UIView { var parentViewController: UIViewController? { sequence(first: self) { $0.next }.first(where: { $0 is UIViewController }) as? UIViewController } }
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

