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

如何确保异步函数内操作完成后再执行视图状态赋值?

问题背景

我有如下简化模型:

struct PostDetailResponse: Codable {
    var post_detail: PostDetailContent
}

struct PostDetailContent: Codable {
    var userHasLiked: Bool?
    enum CodingKeys: String, CodingKey {
        case userHasLiked = "user_has_liked"
    }
}
// 应用内使用的完整数据结构
struct PostDetailed {
    var userHasLiked: Bool?
}
extension PostDetailed {
    mutating func update(with postDetailContent: PostDetailContent) {
        self.userHasLiked = postDetailContent.userHasLiked
    }
}

该模型由ViewModel中的selectPost异步函数填充,ViewModel代码如下:

import Foundation
import SwiftUI
import KeychainAccess
import Kingfisher
class PostDetailedPageViewModel: ObservableObject {
    
    @Published var postDetailed: PostDetailed
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    init(postDetailed: PostDetailed) {
        self.postDetailed = postDetailed
    }
    
    func selectPost(postID: String) async {
        print("This should happen FIRST")
        DispatchQueue.main.async {
            self.isLoading = true
        }
        
        let keychainHelper = KeychainHelperUtility(service: "com.venividi.app")
        
        do {
            guard let accessToken = try keychainHelper.getAccessToken() else {
                DispatchQueue.main.async {
                    self.errorMessage = "Error retrieving access token"
                    self.isLoading = false
                }
                return
            }
            // 使用async/await调用API服务
            let data = try await APIGetService(token: accessToken).invokeGetUserPostDetailedLambda(postID: postID)
            DispatchQueue.main.async {
                self.isLoading = false
                if let data = data {
                    self.updatePostDetail(with: data)
                } else {
                    self.errorMessage = "No data received from the server"
                }
            }
        } catch {
            DispatchQueue.main.async {
                self.isLoading = false
                // 根据错误类型调整错误信息
                if let error = error as? URLError {
                    self.errorMessage = "Networking error: \(error.localizedDescription)"
                    print(self.errorMessage as Any)
                    print("we fuckin up")
                } else {
                    self.errorMessage = "Error: \(error.localizedDescription)"
                    print("we fuckin uppp")
                }
            }
        }
    }
    
    private func updatePostDetail(with data: Data) {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601MilliSeconds)
        
        do {
            let jsonResponse = try decoder.decode(PostDetailResponse.self, from: data)
            let postDetail = jsonResponse.post_detail
            print("we got all of the detail and its here \(postDetail)")
            
            DispatchQueue.main.async {
                // 直接从解码后的数据更新内容和locationsJson
                self.postDetailed.userHasLiked = postDetail.userHasLiked
                print("This should happen SECOND")
            }
        } catch {
            print("there was an issue")
            DispatchQueue.main.async {
                self.errorMessage = "Failed to decode post details: \(error.localizedDescription)"
                print(self.errorMessage ?? "Failed")
            }
        }
    }
}

视图通过.onAppear调用selectPost函数,代码如下:

struct PostDetailedPageView: View {

    @StateObject var viewModel: PostDetailedPageViewModel
    
    init(postDetailed: PostDetailed) {
        _viewModel = StateObject(wrappedValue: PostDetailedPageViewModel(postDetailed: postDetailed))
    }
    
    @State private var isMapExpanded = false
    @State private var isLiked = false  // 管理点赞按钮的状态

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 20) {
                // 显示点赞数和心形按钮
                HStack {
                    Button(action: {
                        isLiked.toggle()
                    }) {
                        Image(systemName: isLiked ? "heart.fill" : "heart")
                            .foregroundColor(isLiked ? .red : .gray)
                    }
                    Text("\(viewModel.postDetailed.likesCount ?? 0) likes")
                        .font(.caption)
                        .foregroundColor(.gray)
                }

            }
            .onAppear {
                Task {
                    await viewModel.selectPost(postID: viewModel.postDetailed.postID)
                    print("This should happen THIRD \(viewModel.postDetailed.userHasLiked ?? nil)")
                    if let userHasLiked = viewModel.postDetailed.userHasLiked {
                        DispatchQueue.main.async {
                            isLiked = userHasLiked
                            print("isLiked is now set to \(isLiked)")
                        }
                    } else {
                        print("userHasLiked is nil")
                    }
                }
            }
        }
        .padding()
        .navigationTitle("Post Details")
        .navigationBarTitleDisplayMode(.inline)
    }
}

问题描述

我预期的执行顺序是:

  1. This should happen FIRST
  2. This should happen SECOND
  3. This should happen THIRD

按这个顺序的话,视图的@State private var isLiked会被正确赋值为viewModel.postDetailed.userHasLiked的值,但实际执行顺序是:

  1. This should happen FIRST
  2. This should happen THIRD
  3. This should happen SECOND

这导致isLiked始终被赋值为nil。我搞不懂为什么isLiked = userHasLiked会在updatePostDetail执行完成前运行,明明我已经用await等待异步函数selectPost完成了。怎么确保执行顺序正确?我希望保留@State变量的使用,而不是直接在视图中使用viewModel.postDetailed.userHasLiked。

注:已确认invokeGetUserPostDetailedLambda能正确获取数据库数据,模型中的mutating func update...是因为实际包含更多字段,这里简化是为了聚焦问题。


解决方案

问题核心是:selectPost函数在API请求完成后,只是把updatePostDetail的调用提交到主队列异步执行,然后就直接返回了——await viewModel.selectPost(...)只会等待API请求和异步任务的提交,不会等待任务实际执行完成,所以才会出现顺序颠倒的情况。

步骤1:改造ViewModel,确保同步等待所有更新完成

用@MainActor标记ViewModel,让所有@Published属性的更新自动在主队列执行,同时让selectPost等待updatePostDetail的所有操作完成:

@MainActor
class PostDetailedPageViewModel: ObservableObject {
    
    @Published var postDetailed: PostDetailed
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    init(postDetailed: PostDetailed) {
        self.postDetailed = postDetailed
    }
    
    func selectPost(postID: String) async {
        print("This should happen FIRST")
        isLoading = true // 无需手动套DispatchQueue.main.async,@MainActor保证在主队列执行
        
        let keychainHelper = KeychainHelperUtility(service: "com.venividi.app")
        
        do {
            guard let accessToken = try keychainHelper.getAccessToken() else {
                errorMessage = "Error retrieving access token"
                isLoading = false
                return
            }
            let data = try await APIGetService(token: accessToken).invokeGetUserPostDetailedLambda(postID: postID)
            isLoading = false
            if let data = data {
                // 等待updatePostDetail完全执行完毕后再继续
                await updatePostDetail(with: data)
            } else {
                errorMessage = "No data received from the server"
            }
        } catch {
            isLoading = false
            if let error = error as? URLError {
                errorMessage = "Networking error: \(error.localizedDescription)"
                print(errorMessage as Any)
                print("we fuckin up")
            } else {
                errorMessage = "Error: \(error.localizedDescription)"
                print("we fuckin uppp")
            }
        }
    }
    
    private func updatePostDetail(with data: Data) async {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601MilliSeconds)
        
        do {
            // 把耗时的JSON解码放到后台队列,避免阻塞主UI
            let jsonResponse = try await Task.detached {
                try decoder.decode(PostDetailResponse.self, from: data)
            }.value
            
            let postDetail = jsonResponse.post_detail
            print("we got all of the detail and its here \(postDetail)")
            
            // @MainActor保证这里的属性更新在主队列执行
            self.postDetailed.userHasLiked = postDetail.userHasLiked
            print("This should happen SECOND")
        } catch {
            print("there was an issue")
            errorMessage = "Failed to decode post details: \(error.localizedDescription)"
            print(errorMessage ?? "Failed")
        }
    }
}

步骤2:简化视图中的onAppear逻辑

因为selectPost现在会等待所有更新完成,视图里的代码无需额外的主队列包裹:

.onAppear {
    Task {
        await viewModel.selectPost(postID: viewModel.postDetailed.postID)
        print("This should happen THIRD \(viewModel.postDetailed.userHasLiked ?? nil)")
        if let userHasLiked = viewModel.postDetailed.userHasLiked {
            isLiked = userHasLiked // 视图上下文的Task默认在主队列执行,无需手动切换
            print("isLiked is now set to \(isLiked)")
        } else {
            print("userHasLiked is nil")
        }
    }
}

效果验证

改造后,执行顺序会严格符合你的预期:

  1. This should happen FIRST
  2. This should happen SECOND
  3. This should happen THIRD

isLiked会被正确赋值为viewModel.postDetailed.userHasLiked的真实值。

内容的提问来源于stack exchange,提问作者Runeaway3

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 16:44:53