Skip to content

Commit 4fe0385

Browse files
Update ActorsExample.swift
1 parent 3f9934a commit 4fe0385

1 file changed

Lines changed: 93 additions & 31 deletions

File tree

SwiftUIExamples/SwiftUIExamples/Combine/ActorsExample.swift

Lines changed: 93 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import SwiftUI
22
import Foundation
33

4+
// MARK: - Thread Inspection Utility
5+
func currentThreadInfo(_ label: String = "") -> String {
6+
let thread = Thread.current
7+
return """
8+
\(label.isEmpty ? "" : "\(label): ")
9+
Thread: \(thread.description)
10+
IsMainThread: \(thread.isMainThread)
11+
QualityOfService: \(thread.qualityOfService.rawValue)
12+
"""
13+
}
14+
415
// MARK: - Data Models
516
struct MealCategoryResponse: Decodable {
617
let categories: [Food]
@@ -19,52 +30,76 @@ struct Food: Identifiable, Decodable, Hashable {
1930
case description = "strCategoryDescription"
2031
}
2132
}
22-
// MARK: - Actor
2333

24-
/// Actor that manages fetching and storing food items in a thread-safe way.
34+
// MARK: - Actor
2535
actor FoodStore {
2636
private(set) var foods: [Food] = []
2737

2838
func fetchFoodData() async throws {
39+
print(currentThreadInfo("FoodStore.fetchFoodData start"))
40+
2941
guard let url = URL(string: "https://github.com/janeshsutharios/REST_GET_API/raw/refs/heads/main/food.json") else {
3042
throw URLError(.badURL)
3143
}
3244

33-
let (data, _) = try await URLSession.shared.data(from: url)
45+
let (data, response) = try await URLSession.shared.data(from: url)
46+
print(currentThreadInfo("FoodStore received network response"))
47+
48+
guard let httpResponse = response as? HTTPURLResponse,
49+
httpResponse.statusCode == 200 else {
50+
throw URLError(.badServerResponse)
51+
}
52+
3453
let decodedFoods = try JSONDecoder().decode(MealCategoryResponse.self, from: data)
3554
self.foods = decodedFoods.categories
55+
56+
print(currentThreadInfo("FoodStore decode completed"))
3657
}
3758

3859
func getFoods() -> [Food] {
60+
print(currentThreadInfo("FoodStore.getFoods"))
3961
return foods
4062
}
4163
}
4264

4365
// MARK: - ViewModel
4466
@MainActor
45-
class FoodViewModel: ObservableObject {
46-
@Published var items: [Food] = []
47-
@Published var error: String? = nil
67+
final class FoodViewModel: ObservableObject {
68+
@Published private(set) var items: [Food] = []
69+
@Published private(set) var error: String? = nil
70+
@Published private(set) var lastThreadInfo: String = ""
71+
4872
private let store = FoodStore()
4973

5074
func load() {
75+
print(currentThreadInfo("FoodViewModel.load start"))
76+
lastThreadInfo = currentThreadInfo("Load initiated")
77+
5178
Task {
5279
let result = await getFoods()
80+
5381
switch result {
5482
case .success(let foods):
83+
print(currentThreadInfo("FoodViewModel before MainActor assignment"))
5584
await MainActor.run {
5685
self.items = foods
5786
self.error = nil
87+
self.lastThreadInfo = currentThreadInfo("MainActor update completed")
88+
print(self.lastThreadInfo)
5889
}
5990
case .failure(let err):
6091
await MainActor.run {
6192
self.error = err.localizedDescription
93+
self.lastThreadInfo = currentThreadInfo("Error handling")
94+
print(self.lastThreadInfo)
6295
}
6396
}
6497
}
6598
}
6699

67100
nonisolated func getFoods() async -> Result<[Food], Error> {
101+
print(currentThreadInfo("FoodViewModel.getFoods start"))
102+
68103
do {
69104
try await store.fetchFoodData()
70105
let foods = await store.getFoods()
@@ -73,53 +108,80 @@ class FoodViewModel: ObservableObject {
73108
return .failure(error)
74109
}
75110
}
76-
77111
}
78112

79113
// MARK: - View
80-
81114
struct FoodListView: View {
82115
@StateObject private var viewModel = FoodViewModel()
83116

84117
var body: some View {
85-
NavigationView {
86-
List(viewModel.items) { item in
87-
HStack(alignment: .top, spacing: 12) {
88-
AsyncImage(url: item.thumbnailURL) { image in
89-
image
90-
.resizable()
91-
.aspectRatio(contentMode: .fill)
92-
.frame(width: 90, height: 90)
93-
.clipShape(Circle())
94-
} placeholder: {
95-
ProgressView()
96-
}
97-
VStack(alignment: .leading, spacing: 4) {
98-
Text(item.name)
99-
.font(.headline)
100-
Text(item.description)
101-
.font(.subheadline)
102-
.foregroundColor(.gray)
118+
NavigationStack {
119+
List {
120+
Section("Thread Info") {
121+
Text(viewModel.lastThreadInfo)
122+
.font(.caption)
123+
.monospaced()
124+
}
125+
126+
Section("Food Items") {
127+
ForEach(viewModel.items) { item in
128+
HStack(alignment: .top, spacing: 12) {
129+
AsyncImage(url: item.thumbnailURL) { phase in
130+
switch phase {
131+
case .empty:
132+
ProgressView()
133+
case .success(let image):
134+
image
135+
.resizable()
136+
.aspectRatio(contentMode: .fill)
137+
.frame(width: 90, height: 90)
138+
.clipShape(Circle())
139+
case .failure:
140+
Image(systemName: "photo")
141+
.frame(width: 90, height: 90)
142+
@unknown default:
143+
EmptyView()
144+
}
145+
}
146+
147+
VStack(alignment: .leading, spacing: 4) {
148+
Text(item.name)
149+
.font(.headline)
150+
Text(item.description)
151+
.font(.subheadline)
152+
.foregroundColor(.gray)
153+
.lineLimit(2)
154+
}
155+
}
103156
}
104157
}
105158
}
106159
.navigationTitle("🍱 Food Menu")
107-
.onAppear {
160+
.refreshable {
161+
print(currentThreadInfo("Pull-to-refresh start"))
162+
viewModel.load()
163+
}
164+
.task {
165+
print(currentThreadInfo("Initial load start"))
108166
viewModel.load()
109167
}
110168
.overlay {
111169
if let error = viewModel.error {
112-
Text("Error: \(error)")
113-
.foregroundColor(.red)
114-
.padding()
170+
VStack {
171+
Text("Error: \(error)")
172+
.foregroundColor(.red)
173+
Button("Retry") {
174+
viewModel.load()
175+
}
176+
}
177+
.padding()
115178
}
116179
}
117180
}
118181
}
119182
}
120183

121184
// MARK: - Preview
122-
123185
struct ContentView_Previews: PreviewProvider {
124186
static var previews: some View {
125187
FoodListView()

0 commit comments

Comments
 (0)