ios widget showing what is available at chucks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: handle error states

+201 -6
+4 -4
wasup-chucks.xcodeproj/project.pbxproj
··· 375 375 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 376 376 ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 377 377 CODE_SIGN_STYLE = Automatic; 378 - CURRENT_PROJECT_VERSION = 2; 378 + CURRENT_PROJECT_VERSION = 3; 379 379 DEVELOPMENT_TEAM = M67B42LX8D; 380 380 ENABLE_PREVIEWS = YES; 381 381 GENERATE_INFOPLIST_FILE = YES; ··· 410 410 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 411 411 ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 412 412 CODE_SIGN_STYLE = Automatic; 413 - CURRENT_PROJECT_VERSION = 2; 413 + CURRENT_PROJECT_VERSION = 3; 414 414 DEVELOPMENT_TEAM = M67B42LX8D; 415 415 ENABLE_PREVIEWS = YES; 416 416 GENERATE_INFOPLIST_FILE = YES; ··· 444 444 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 445 445 ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 446 446 CODE_SIGN_STYLE = Automatic; 447 - CURRENT_PROJECT_VERSION = 2; 447 + CURRENT_PROJECT_VERSION = 3; 448 448 DEVELOPMENT_TEAM = M67B42LX8D; 449 449 GENERATE_INFOPLIST_FILE = YES; 450 450 INFOPLIST_FILE = widget/Info.plist; ··· 474 474 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 475 475 ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 476 476 CODE_SIGN_STYLE = Automatic; 477 - CURRENT_PROJECT_VERSION = 2; 477 + CURRENT_PROJECT_VERSION = 3; 478 478 DEVELOPMENT_TEAM = M67B42LX8D; 479 479 GENERATE_INFOPLIST_FILE = YES; 480 480 INFOPLIST_FILE = widget/Info.plist;
+114
wasup-chucks.xcodeproj/xcshareddata/xcschemes/widgetExtension.xcscheme
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <Scheme 3 + LastUpgradeVersion = "2620" 4 + wasCreatedForAppExtension = "YES" 5 + version = "2.0"> 6 + <BuildAction 7 + parallelizeBuildables = "YES" 8 + buildImplicitDependencies = "YES" 9 + buildArchitectures = "Automatic"> 10 + <BuildActionEntries> 11 + <BuildActionEntry 12 + buildForTesting = "YES" 13 + buildForRunning = "YES" 14 + buildForProfiling = "YES" 15 + buildForArchiving = "YES" 16 + buildForAnalyzing = "YES"> 17 + <BuildableReference 18 + BuildableIdentifier = "primary" 19 + BlueprintIdentifier = "0BBF20342F2D46AF00AF0585" 20 + BuildableName = "widgetExtension.appex" 21 + BlueprintName = "widgetExtension" 22 + ReferencedContainer = "container:wasup-chucks.xcodeproj"> 23 + </BuildableReference> 24 + </BuildActionEntry> 25 + <BuildActionEntry 26 + buildForTesting = "YES" 27 + buildForRunning = "YES" 28 + buildForProfiling = "YES" 29 + buildForArchiving = "YES" 30 + buildForAnalyzing = "YES"> 31 + <BuildableReference 32 + BuildableIdentifier = "primary" 33 + BlueprintIdentifier = "0BBF201C2F2D443D00AF0585" 34 + BuildableName = "wasup-chucks.app" 35 + BlueprintName = "wasup-chucks" 36 + ReferencedContainer = "container:wasup-chucks.xcodeproj"> 37 + </BuildableReference> 38 + </BuildActionEntry> 39 + </BuildActionEntries> 40 + </BuildAction> 41 + <TestAction 42 + buildConfiguration = "Debug" 43 + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 44 + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 45 + shouldUseLaunchSchemeArgsEnv = "YES" 46 + shouldAutocreateTestPlan = "YES"> 47 + </TestAction> 48 + <LaunchAction 49 + buildConfiguration = "Debug" 50 + selectedDebuggerIdentifier = "" 51 + selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" 52 + launchStyle = "0" 53 + askForAppToLaunch = "Yes" 54 + useCustomWorkingDirectory = "NO" 55 + ignoresPersistentStateOnLaunch = "NO" 56 + debugDocumentVersioning = "YES" 57 + debugServiceExtension = "internal" 58 + allowLocationSimulation = "YES" 59 + launchAutomaticallySubstyle = "2"> 60 + <BuildableProductRunnable 61 + runnableDebuggingMode = "0"> 62 + <BuildableReference 63 + BuildableIdentifier = "primary" 64 + BlueprintIdentifier = "0BBF201C2F2D443D00AF0585" 65 + BuildableName = "wasup-chucks.app" 66 + BlueprintName = "wasup-chucks" 67 + ReferencedContainer = "container:wasup-chucks.xcodeproj"> 68 + </BuildableReference> 69 + </BuildableProductRunnable> 70 + <EnvironmentVariables> 71 + <EnvironmentVariable 72 + key = "_XCWidgetKind" 73 + value = "" 74 + isEnabled = "YES"> 75 + </EnvironmentVariable> 76 + <EnvironmentVariable 77 + key = "_XCWidgetDefaultView" 78 + value = "timeline" 79 + isEnabled = "YES"> 80 + </EnvironmentVariable> 81 + <EnvironmentVariable 82 + key = "_XCWidgetFamily" 83 + value = "systemMedium" 84 + isEnabled = "YES"> 85 + </EnvironmentVariable> 86 + </EnvironmentVariables> 87 + </LaunchAction> 88 + <ProfileAction 89 + buildConfiguration = "Release" 90 + shouldUseLaunchSchemeArgsEnv = "YES" 91 + savedToolIdentifier = "" 92 + useCustomWorkingDirectory = "NO" 93 + debugDocumentVersioning = "YES" 94 + askForAppToLaunch = "Yes" 95 + launchAutomaticallySubstyle = "2"> 96 + <BuildableProductRunnable 97 + runnableDebuggingMode = "0"> 98 + <BuildableReference 99 + BuildableIdentifier = "primary" 100 + BlueprintIdentifier = "0BBF201C2F2D443D00AF0585" 101 + BuildableName = "wasup-chucks.app" 102 + BlueprintName = "wasup-chucks" 103 + ReferencedContainer = "container:wasup-chucks.xcodeproj"> 104 + </BuildableReference> 105 + </BuildableProductRunnable> 106 + </ProfileAction> 107 + <AnalyzeAction 108 + buildConfiguration = "Debug"> 109 + </AnalyzeAction> 110 + <ArchiveAction 111 + buildConfiguration = "Release" 112 + revealArchiveInOrganizer = "YES"> 113 + </ArchiveAction> 114 + </Scheme>
+18 -2
wasup-chucks/ChucksModels.swift
··· 285 285 throw ChucksError.networkError 286 286 } 287 287 288 - let menu = try JSONDecoder().decode(MenuResponse.self, from: data) 288 + let menu: MenuResponse 289 + do { 290 + menu = try JSONDecoder().decode(MenuResponse.self, from: data) 291 + } catch { 292 + throw ChucksError.decodingError 293 + } 289 294 cachedMenu = menu 290 295 cacheDate = Date() 291 296 ··· 309 314 } 310 315 } 311 316 312 - enum ChucksError: Error { 317 + enum ChucksError: Error, LocalizedError { 313 318 case invalidURL 314 319 case networkError 315 320 case decodingError 321 + 322 + var errorDescription: String? { 323 + switch self { 324 + case .invalidURL: 325 + return "Invalid URL" 326 + case .networkError: 327 + return "Network error" 328 + case .decodingError: 329 + return "Failed to parse menu data" 330 + } 331 + } 316 332 }
+65
wasup-chucks/ContentView.swift
··· 14 14 @State private var status = ChucksStatus.calculate() 15 15 @State private var todayMenu: [VenueMenu] = [] 16 16 @State private var isLoading = true 17 + @State private var loadError: Error? = nil 17 18 @State private var selectedMeal: MealSchedule? = nil 18 19 @Environment(\.horizontalSizeClass) private var horizontalSizeClass 19 20 ··· 54 55 if isLoading { 55 56 ProgressView() 56 57 .frame(maxWidth: .infinity, minHeight: 200) 58 + } else if let error = loadError { 59 + ErrorCard(error: error) { 60 + Task { await loadMenu() } 61 + } 57 62 } else { 58 63 CurrentMealView(menu: todayMenu, slot: currentSlot, isOpen: status.isOpen, isRegularWidth: isRegularWidth) 59 64 } ··· 81 86 82 87 func loadMenu() async { 83 88 isLoading = true 89 + loadError = nil 84 90 do { 85 91 let menu = try await ChucksService.shared.fetchMenu() 86 92 let dateFormatter = DateFormatter() ··· 89 95 let dateKey = dateFormatter.string(from: Date()) 90 96 todayMenu = menu[dateKey] ?? [] 91 97 } catch { 98 + loadError = error 92 99 print("Failed to load menu: \(error)") 93 100 } 94 101 isLoading = false ··· 139 146 } 140 147 .padding(16) 141 148 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) 149 + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16)) 150 + .shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 2) 151 + } 152 + } 153 + 154 + // MARK: - Error Card 155 + 156 + struct ErrorCard: View { 157 + let error: Error 158 + let retry: () -> Void 159 + 160 + private var errorMessage: String { 161 + if let chucksError = error as? ChucksError { 162 + switch chucksError { 163 + case .invalidURL: 164 + return "There was a problem with the request." 165 + case .networkError: 166 + return "Couldn't connect to the server. Check your internet connection." 167 + case .decodingError: 168 + return "The menu data couldn't be read." 169 + } 170 + } 171 + return "Something went wrong loading the menu." 172 + } 173 + 174 + var body: some View { 175 + VStack(spacing: 16) { 176 + Image(systemName: "wifi.exclamationmark") 177 + .font(.system(size: 40)) 178 + .foregroundStyle(.secondary) 179 + 180 + Text("Menu Unavailable") 181 + .font(.headline) 182 + 183 + Text(errorMessage) 184 + .font(.subheadline) 185 + .foregroundStyle(.secondary) 186 + .multilineTextAlignment(.center) 187 + 188 + Button(action: retry) { 189 + Label("Try Again", systemImage: "arrow.clockwise") 190 + .font(.subheadline.weight(.medium)) 191 + } 192 + .buttonStyle(.borderedProminent) 193 + .tint(.orange) 194 + } 195 + .padding(24) 196 + .frame(maxWidth: .infinity) 142 197 .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16)) 143 198 .shadow(color: .black.opacity(0.08), radius: 8, x: 0, y: 2) 144 199 } ··· 504 559 #Preview { 505 560 ContentView() 506 561 } 562 + 563 + #Preview("Error - Network") { 564 + ErrorCard(error: ChucksError.networkError) {} 565 + .padding() 566 + } 567 + 568 + #Preview("Error - Decoding") { 569 + ErrorCard(error: ChucksError.decodingError) {} 570 + .padding() 571 + }