11import SwiftUI
22import 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
516struct 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
2535actor 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-
81114struct 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-
123185struct ContentView_Previews : PreviewProvider {
124186 static var previews : some View {
125187 FoodListView ( )
0 commit comments