I have following (simplified) model:
struct PostDetailResponse: Codable {
var post_detail: PostDetailContent
}
struct PostDetailContent: Codable {
var userHasLiked: Bool?
enum CodingKeys: String, CodingKey {
case userHasLiked = "user_has_liked"
}
}
// Full data structure used within the application
struct PostDetailed {
var userHasLiked: Bool?
}
extension PostDetailed {
mutating func update(with postDetailContent: PostDetailContent) {
self.userHasLiked = postDetailContent.userHasLiked
}
}
It is populate via selectPost
which is defined in the 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
}
// Using async/await to call the API service
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
// Adjust error message based on the type of error
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 {
// Update the content and locationsJson directly from the decoded data
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")
}
}
}
}
The view runs the selectPost
in .onAppear
since this page is navigated to from a list of a variety of posts.
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 // State to manage the like button
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
// Display the number of likes with heart button
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)
}
}
I am expecting the order of operations to follow those described in the print statements:
- This should happen FIRST
- This should happen SECOND
- This should happen THIRD
If this worked in the right order, we would be assigning the view
‘s @State private var isLiked
to the value properly populated into viewModel.postDetailed.userHasLiked
from updatePostDetail
. Unfortunately, the order occurs as follows:
- This should happen FIRST
- This should happen THIRD
- This should happen SECOND
This leads to isLiked
always being assigned nil
. I don’t understand why the isLiked = userHasLiked
is running before updatePostDetail
has finished executing. The latter is tucked inside of an async function selectPost
which we are explicitly saying to await
. Why would the isLiked = userHasLiked
run before all parts of selectPost
– including the nested updatePostDetail
– are completed?
How can I ensure this works properly? I have thought to simply use viewModel.postDetailed.userHasLiked
directly within the view, but then I lose out on the benefits of using the @State
variable in the view and this seems like bad practice.
Note I have ensured that the data is being properly populated and called from the database via invokeGetUserPostDetailedLambda
, so you can assume this works properly.