如何确保异步函数内操作完成后再执行视图状态赋值?
问题背景
我有如下简化模型:
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) } }
问题描述
我预期的执行顺序是:
- This should happen FIRST
- This should happen SECOND
- This should happen THIRD
按这个顺序的话,视图的@State private var isLiked会被正确赋值为viewModel.postDetailed.userHasLiked的值,但实际执行顺序是:
- This should happen FIRST
- This should happen THIRD
- 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") } } }
效果验证
改造后,执行顺序会严格符合你的预期:
- This should happen FIRST
- This should happen SECOND
- This should happen THIRD
isLiked会被正确赋值为viewModel.postDetailed.userHasLiked的真实值。
内容的提问来源于stack exchange,提问作者Runeaway3

