You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

兼容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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 14:02:01