My understanding is the TripsFeedViewModel runs on the MainActor due to observable so this class is basically isolated to an actor, the MainActor. So in a sense it does prevent data races. Then when we tripService.getTrips() the main thread will pass it to a a background thread to wait in the queue/lock of the TripService actor. Once the thread is allowed in the TripService actor it initializes the NetworkRequestService which is then bound to that actor and so would it’s functions. So when we call sendRequest which is a method of the NetworkRequestService it would run on the TripService actor and the thread would wait until there is a response back, holding how the lock. However, is we mark is as nonisolated it can be passed to a background thread to complete then a thread can pick it up and enter the queue again. so that’s why we have to mark it as nonisolated and the NetworkRequestService as sendable because its safe to send across threads since there are no mutable states. Then when it comes to the PrivateTripResponse it is technically always sendable because its a struct and a copy is created instead of having references to it. Since PrivateTripResponse conforms to Codable it runs on the MainActor so marking it as nonisolated lets it get decoded on a background actor as well and its safe because its sendable. So sendbale and nonisolated is working hand and hand letting the complier know is something is thread-safe to send across threads with data races. Would this explanation capture what is going on in this entire flow?
import Foundation
@Observable
class TripsFeedViewModel {
var trips: [Trip] = []
private let tripService: TripServiceProtocol
init(tripService: TripServiceProtocol) {
self.tripService = tripService
}
func getTrip() async -> Void {
do {
let trips = try await tripService.getTrips()
await MainActor.run {
self.trips = trips.compactMap {
Trip(
id: $0.id,
tripName: $0.tripName,
location: $0.location,
budget: $0.budget,
isFavorite: $0.isFavorite,
startDate: $0.startDate,
endDate: $0.endDate,
imageURLString: $0.imageURL
)
}
}
} catch {
print("There was an error get your trips: \(error.localizedDescription)")
}
}
}
import Foundation
actor TripService: TripServiceProtocol {
private let networkService: NetworkRequestService
private let keychainService: KeychainService
private var activeTask: Task<[TripPrivateResponse], Error>?
init(networkService: NetworkRequestService, keychainService: KeychainService) {
self.networkService = networkService
self.keychainService = keychainService
}
func getTrips() async throws -> [TripPrivateResponse] {
if let existing = activeTask {
return try await existing.value
}
let task = Task<[TripPrivateResponse], Error> {
guard let url = URL(string: "local host url") else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
if let token = keychainService.getToken() {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return try await networkService.sendRequest(request: request, responseType: [TripPrivateResponse].self)
}
activeTask = task
defer { activeTask = nil }
return try await task.value
}
}
struct TripPrivateResponse: nonisolated Codable, Sendable {
let id: Int
let tripName: String
let location: String
let budget: Int
let isFavorite: Bool
let startDateString: String
let endDateString: String
let imageURLString: String
enum CodingKeys: String, CodingKey {
case id
case tripName = "title"
case location
case budget
case isFavorite = "is_favorite"
case startDateString = "start_date"
case endDateString = "end_date"
case imageURLString = "cover_image_url"
}
}
final class NetworkRequestService: Sendable {
// MARK: Sends the request and returns the response from FastAPI
nonisolated func sendRequest(request: URLRequest, responseType: Output.Type) async throws -> Output {
do {
/// 1. this sends the data to FastAPI then waits for a response
let (data, response) = try await URLSession.shared.data(for: request)
print("FASTAPI RESPONSE: \(String(data: data, encoding: .utf8) ?? "No Data")")
/// 2. checks the response (convert it to HTTPURLResponse type) making sure it has a successful status code
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
throw APIError.invalidResponse
}
/// 3. decode it to the DTO (UserPrivateResponse)
return try JSONDecoder().decode(responseType, from: data)
} catch let error as URLError {
throw APIError.networkError(error)
} catch let error as DecodingError {
throw APIError.decoding(error)
}
}
}