如何判断SwiftUI视图转场/动画已完成?
SwiftUI 等待视图转场完成后展示模态框的优雅方案
问题场景
关闭Sheet A后从View A转场至View B,若直接在View B初始化时绑定状态展示Sheet B,会触发以下错误:
[Presentation] Attempt to present <TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView: 0x107a03a00> on <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_: 0x107821800> (from <TtGC7SwiftUI19UIHostingControllerVVS_19BridgedPresentation8RootView: 0x107a1a200>) while a presentation is in progress.
这是因为View B的转场动画尚未完成,此时触发新的模态框展示会导致冲突。以下是两种比硬编码延迟更可靠的解决方案:
方案一:利用withAnimation的Completion回调(iOS 15+)
SwiftUI从iOS 15开始支持withAnimation的完成闭包,可精准监听转场动画结束时机:
struct ContentView: View { @State private var showSheetA = false @State private var showViewB = false @State private var showSheetB = false var body: some View { ZStack { if !showViewB { ViewA() .sheet(isPresented: $showSheetA) { SheetA(onButtonTap: handleSheetAButtonTap) } } else { ViewB() .sheet(isPresented: $showSheetB) { SheetB() } } } } private func handleSheetAButtonTap() { // 先关闭Sheet A showSheetA = false // 执行View A到View B的转场动画,完成后触发Sheet B withAnimation { showViewB = true } completion: { showSheetB = true } } } // 示例子视图 struct ViewA: View { var body: some View { Text("View A") .onTapGesture { showSheetA = true } } } struct SheetA: View { let onButtonTap: () -> Void var body: some View { Button("关闭并转场到View B,展示Sheet B") { onButtonTap() } } } struct ViewB: View { var body: some View { Text("View B(Sheet B的背景)") } } struct SheetB: View { var body: some View { Text("Sheet B") } }
这种方案完全基于SwiftUI原生API,无需依赖UIKit,逻辑清晰且时机精准。
方案二:自定义Modifier监听转场完成(兼容iOS 13+)
如果需要支持iOS 13及以下版本,可以通过自定义ViewModifier结合UIKit的动画监听来实现:
// 自定义Modifier,监听视图转场完成 struct OnTransitionCompletedModifier: ViewModifier { let action: () -> Void func body(content: Content) -> some View { content .background(TransitionCompletionView(action: action)) } } // 借助UIViewRepresentable监听动画完成 struct TransitionCompletionView: UIViewRepresentable { let action: () -> Void func makeUIView(context: Context) -> UIView { let view = UIView() view.isHidden = true return view } func updateUIView(_ uiView: UIView, context: Context) { // 通过空动画触发完成回调,监听视图转场结束 UIView.animate(withDuration: 0) {} completion: { _ in DispatchQueue.main.async { self.action() } } } } // 给View添加扩展方法,方便调用 extension View { func onTransitionCompleted(action: @escaping () -> Void) -> some View { modifier(OnTransitionCompletedModifier(action: action)) } } // 使用示例 struct ContentView: View { @State private var showSheetA = false @State private var showViewB = false @State private var showSheetB = false var body: some View { ZStack { if !showViewB { ViewA() .sheet(isPresented: $showSheetA) { SheetA(onButtonTap: { showSheetA = false withAnimation { showViewB = true } }) } } else { ViewB() .onTransitionCompleted { showSheetB = true } .sheet(isPresented: $showSheetB) { SheetB() } } } } }
这种方案通过UIKit的动画回调机制,能准确捕捉到View B转场完成的时机,兼容更低版本的iOS系统。
方案对比
- 硬编码延迟(
asyncAfter):依赖固定时间值,不同设备/系统版本的动画时长不一致,容易出现报错或等待过长的问题。 - Sheet内导航:无法让View B作为Sheet B的背景提供上下文,不符合需求。
- 上述两种方案:精准监听转场完成时机,避免冲突,同时满足View B作为背景的需求。
内容的提问来源于stack exchange,提问作者Haensl
相关产品推荐
相关产品推荐

