iOS 16中NavigationStack滚动视图惯性滚动返回崩溃问题求助
问题本质
这是iOS 16.x版本SwiftUI的已知bug:当绑定到NavigationStack的NavigationPath(存储在ObservableObject的@Published属性中),在ScrollView处于惯性滚动状态时被修改(比如点击返回按钮触发路径弹出),会出现并发读写冲突,触发Simultaneous accesses崩溃。iOS 17.3及以上版本苹果已修复该问题。
解决方案
方案1:使用本地@State管理导航路径(最简方案)
将导航路径从全局ObservableObject迁移到RootView的本地@State变量,避免跨对象属性绑定带来的线程安全风险:
struct RootView: View { @State private var path = NavigationPath() var body: some View { NavigationStack(path: $path) { VStack { Button("Go to child") { path.append(ChildRoute()) } .buttonStyle(.borderedProminent) } .navigationDestination(for: ChildRoute.self) { _ in ScrollView { LazyVStack { Color.indigo.frame(height: 400) Color.red.frame(height: 400) Color.green.frame(height: 400) Color.purple.frame(height: 400) Color.orange.frame(height: 400) } } } } } } struct ChildRoute: Hashable { }
方案2:给ObservableObject的路径添加串行队列保护
如果必须通过ObservableObject统一管理导航逻辑,用串行队列确保对path的读写操作原子执行,避免并发冲突:
class Communicator: ObservableObject { private let navigationQueue = DispatchQueue(label: "com.yourapp.navigation.queue") private var _path = NavigationPath() @Published var path: NavigationPath { get { navigationQueue.sync { _path } } set { navigationQueue.sync { _path = newValue } } } func navigate() { navigationQueue.async { [weak self] in guard let self = self else { return } self._path.append(ChildRoute()) // 手动发送更新通知,确保SwiftUI同步视图 DispatchQueue.main.async { self.objectWillChange.send() } } } }
通过私有串行队列封装_path的读写,避免@Published内部的并发访问问题,同时手动触发视图更新。
方案3:自定义返回按钮,先中断ScrollView惯性滚动
在Child视图中自定义返回按钮,强制停止ScrollView的惯性滚动后再执行导航返回,切断滚动回调与路径修改的并发时机:
// 封装可控制滚动状态的ScrollView struct ControlledScrollView<Content: View>: View { @State private var scrollProxy: ScrollViewProxy? let content: () -> Content var body: some View { ScrollView { ScrollViewReader { proxy in content() .onAppear { scrollProxy = proxy } } } .onDisappear { // 强制滚动到顶部,中断惯性滚动 scrollProxy?.scrollTo(0, anchor: .top, animation: .none) } } } // 在RootView的navigationDestination中使用 .navigationDestination(for: ChildRoute.self) { _ in ControlledScrollView { LazyVStack { Color.indigo.frame(height: 400) Color.red.frame(height: 400) Color.green.frame(height: 400) Color.purple.frame(height: 400) Color.orange.frame(height: 400) } } .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Back") { communicator.path.removeLast() } } } }
通过ScrollViewReader获取滚动代理,在视图消失或返回前强制终止惯性滚动,避免SwiftUI在滚动回调中访问导航路径。
内容的提问来源于stack exchange,提问作者FunnyHorse
相关产品推荐
相关产品推荐

