SwiftUI中如何结合onAppear()/task()与视图transition()使用?
SwiftUI转场动画与数据加载冲突问题解决
问题根源
你遇到的问题核心在于:DataView的转场动画尚未完成时,内部loaded状态就发生了变更。SwiftUI的视图更新依赖状态变化驱动,当转场动画进行中,loaded从false变为true会触发DataView内部视图结构重新计算,这会打断SwiftUI对原有转场动画的跟踪,导致数据视图直接“跳”出,而非延续转场的平滑动画。
解决方案
提供两种可行的解决思路:
思路1:等待转场动画结束后再加载数据
通过监听转场动画的完成事件,在动画彻底结束后再触发数据加载,避免状态变更打断动画流程。
方式A:iOS 17+ 原生API
iOS 17及以上版本的withAnimation支持传入completion闭包,可直接在动画结束后通知DataView开始加载数据:
import SwiftUI import PlaygroundSupport struct MainView: View { @State var show = false @State var canLoadData = false // 控制DataView是否允许加载数据 var body: some View { VStack { if show { DataView(canLoadData: canLoadData) .transition(.move(edge: .leading)) } Spacer() Button("Show data") { withAnimation(.linear(duration: 4)) { show.toggle() } completion: { // 动画结束后允许加载数据 canLoadData = true } } } } } struct DataView: View { let canLoadData: Bool @State var loaded = false var body: some View { VStack { Text("Data view") if loaded { Text("The data") } else { LoaderView() } } .task(id: canLoadData) { // 通过id监听canLoadData的变化 guard canLoadData else { return } try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC))) loaded = true } } } struct LoaderView: View { var body: some View { Text("Loading data") } } PlaygroundPage.current.setLiveView(MainView().frame(width: 300, height: 200))
方式B:兼容iOS 13+ 的自定义动画监听Modifier
如果需要兼容更低版本,可以自定义一个监听动画完成的Modifier:
import SwiftUI import PlaygroundSupport // 自定义动画完成监听Modifier struct AnimationCompletionModifier: AnimatableModifier { var animatableData: CGFloat = 0 private let completion: () -> Void init(completion: @escaping () -> Void) { self.completion = completion } func body(content: Content) -> some View { content .onAppear { withAnimation { animatableData = 1 } } .onChange(of: animatableData) { newValue in if newValue == 1 { completion() } } } } extension View { func onAnimationComplete(completion: @escaping () -> Void) -> some View { self.modifier(AnimationCompletionModifier(completion: completion)) } } struct MainView: View { @State var show = false @State var canLoadData = false var body: some View { VStack { if show { DataView(canLoadData: canLoadData) .transition(.move(edge: .leading)) .onAnimationComplete { canLoadData = true } } Spacer() Button("Show data") { withAnimation(.linear(duration: 4)) { show.toggle() } } } } } struct DataView: View { let canLoadData: Bool @State var loaded = false var body: some View { VStack { Text("Data view") if loaded { Text("The data") } else { LoaderView() } } .task(id: canLoadData) { guard canLoadData else { return } try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC))) loaded = true } } } struct LoaderView: View { var body: some View { Text("Loading data") } } PlaygroundPage.current.setLiveView(MainView().frame(width: 300, height: 200))
思路2:让内部视图切换参与动画
如果希望在转场动画过程中就开始加载数据,同时让加载状态的切换也有平滑动画,可以给内部的视图切换添加transition和animation:
import SwiftUI import PlaygroundSupport struct MainView: View { @State var show = false var body: some View { VStack { if show { DataView() .transition(.move(edge: .leading)) } Spacer() Button("Show data") { withAnimation(.linear(duration: 4)) { show.toggle() } } } } } struct DataView: View { @State var loaded = false var body: some View { VStack { Text("Data view") ZStack { // 用ZStack让两个视图在同一位置切换 LoaderView() .opacity(loaded ? 0 : 1) Text("The data") .opacity(loaded ? 1 : 0) } .animation(.linear(duration: 0.5), value: loaded) // 给状态变化添加动画 } .task { try? await Task.sleep(nanoseconds: UInt64(2 * Double(NSEC_PER_SEC))) loaded = true } } } struct LoaderView: View { var body: some View { Text("Loading data") } } PlaygroundPage.current.setLiveView(MainView().frame(width: 300, height: 200))
这种方式下,转场动画和内部的加载状态切换动画会同时进行,数据视图会以淡入的方式出现,不会破坏转场的流畅性。
内容的提问来源于stack exchange,提问作者user121882
相关产品推荐
相关产品推荐

