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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 06:40:34