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(ios): only expire cache every 12 hours and use appgroup

+149 -19
android/.kotlin/sessions/kotlin-compiler-5173800182017658485.salive

This is a binary file and will not be displayed.

+118 -15
ios/Shared/ChucksShared.swift
··· 286 286 } 287 287 } 288 288 289 + // MARK: - Persistent Cache 290 + 291 + private enum MenuCacheIO: Sendable { 292 + nonisolated static func load(from directory: URL) -> (menu: MenuResponse, cacheDate: Date)? { 293 + let menuURL = directory.appendingPathComponent("menu_cache.json") 294 + let metaURL = directory.appendingPathComponent("menu_cache_meta.plist") 295 + 296 + guard FileManager.default.fileExists(atPath: menuURL.path), 297 + FileManager.default.fileExists(atPath: metaURL.path) else { return nil } 298 + 299 + do { 300 + let menuData = try Data(contentsOf: menuURL) 301 + let menu = try JSONDecoder().decode(MenuResponse.self, from: menuData) 302 + 303 + let metaData = try Data(contentsOf: metaURL) 304 + guard let meta = try PropertyListSerialization.propertyList(from: metaData, format: nil) as? [String: Any], 305 + let timestamp = meta["cacheTimestamp"] as? Date else { return nil } 306 + 307 + return (menu, timestamp) 308 + } catch { 309 + return nil 310 + } 311 + } 312 + 313 + nonisolated static func save(menu: MenuResponse, to directory: URL) { 314 + let menuURL = directory.appendingPathComponent("menu_cache.json") 315 + let metaURL = directory.appendingPathComponent("menu_cache_meta.plist") 316 + 317 + do { 318 + let menuData = try JSONEncoder().encode(menu) 319 + try menuData.write(to: menuURL) 320 + 321 + let meta: [String: Any] = ["cacheTimestamp": Date()] 322 + let metaData = try PropertyListSerialization.data(fromPropertyList: meta, format: .binary, options: 0) 323 + try metaData.write(to: metaURL) 324 + } catch { 325 + // Ignore save errors 326 + } 327 + } 328 + 329 + nonisolated static func delete(from directory: URL) { 330 + let menuURL = directory.appendingPathComponent("menu_cache.json") 331 + let metaURL = directory.appendingPathComponent("menu_cache_meta.plist") 332 + try? FileManager.default.removeItem(at: menuURL) 333 + try? FileManager.default.removeItem(at: metaURL) 334 + } 335 + } 336 + 289 337 // MARK: - API Service 290 338 291 339 public actor ChucksService { ··· 294 342 private let baseURL = "https://diningdata.cedarville.edu/api/menus" 295 343 private var cachedMenu: MenuResponse? 296 344 private var cacheDate: Date? 297 - private let cacheExpiration: TimeInterval = 3600 345 + private let cacheExpiration: TimeInterval = 12 * 60 * 60 // 12 hours 346 + 347 + private static let appGroupID = "group.sh.dunkirk.wasup-chucks" 348 + 349 + private var cacheDirectory: URL? { 350 + FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Self.appGroupID) 351 + } 352 + 353 + public init() { 354 + // Load persistent cache on init 355 + Task { await loadPersistentCache() } 356 + } 357 + 358 + private func loadPersistentCache() { 359 + guard let dir = cacheDirectory, 360 + let cached = MenuCacheIO.load(from: dir) else { return } 361 + self.cachedMenu = cached.menu 362 + self.cacheDate = cached.cacheDate 363 + } 364 + 365 + private func savePersistentCache(menu: MenuResponse) { 366 + guard let dir = cacheDirectory else { return } 367 + MenuCacheIO.save(menu: menu, to: dir) 368 + } 298 369 299 - public init() {} 370 + public func invalidateCache() { 371 + cachedMenu = nil 372 + cacheDate = nil 373 + if let dir = cacheDirectory { 374 + MenuCacheIO.delete(from: dir) 375 + } 376 + } 300 377 301 378 public func fetchMenu(days: Int = 5) async throws -> MenuResponse { 379 + // Check in-memory cache first 302 380 if let cached = cachedMenu, 303 381 let date = cacheDate, 304 382 Date().timeIntervalSince(date) < cacheExpiration { 305 383 return cached 306 384 } 307 385 386 + // Check persistent cache 387 + if cachedMenu == nil { 388 + loadPersistentCache() 389 + if let cached = cachedMenu, 390 + let date = cacheDate, 391 + Date().timeIntervalSince(date) < cacheExpiration { 392 + return cached 393 + } 394 + } 395 + 308 396 guard let url = URL(string: "\(baseURL)?days=\(days)") else { 309 397 throw ChucksError.invalidURL 310 398 } ··· 314 402 request.setValue("https://www.cedarville.edu", forHTTPHeaderField: "Origin") 315 403 request.setValue("https://www.cedarville.edu/offices/the-commons", forHTTPHeaderField: "Referer") 316 404 317 - let (data, response) = try await URLSession.shared.data(for: request) 405 + do { 406 + let (data, response) = try await URLSession.shared.data(for: request) 318 407 319 - guard let httpResponse = response as? HTTPURLResponse, 320 - httpResponse.statusCode == 200 else { 321 - throw ChucksError.networkError 322 - } 408 + guard let httpResponse = response as? HTTPURLResponse, 409 + httpResponse.statusCode == 200 else { 410 + // Return stale cache on network error if available 411 + if let cached = cachedMenu { 412 + return cached 413 + } 414 + throw ChucksError.networkError 415 + } 416 + 417 + let menu: MenuResponse 418 + do { 419 + menu = try JSONDecoder().decode(MenuResponse.self, from: data) 420 + } catch { 421 + throw ChucksError.decodingError(underlying: error) 422 + } 423 + cachedMenu = menu 424 + cacheDate = Date() 425 + savePersistentCache(menu: menu) 323 426 324 - let menu: MenuResponse 325 - do { 326 - menu = try JSONDecoder().decode(MenuResponse.self, from: data) 427 + return menu 428 + } catch let error as ChucksError { 429 + throw error 327 430 } catch { 328 - throw ChucksError.decodingError(underlying: error) 431 + // Return stale cache on network error if available 432 + if let cached = cachedMenu { 433 + return cached 434 + } 435 + throw ChucksError.networkError 329 436 } 330 - cachedMenu = menu 331 - cacheDate = Date() 332 - 333 - return menu 334 437 } 335 438 336 439 public func getSpecials(for date: Date, phase: MealPhase) async throws -> [MenuItem] {
+10 -4
ios/wasup-chucks.xcodeproj/project.pbxproj
··· 41 41 0BBF20352F2D46AF00AF0585 /* widgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = widgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 42 42 0BBF20372F2D46AF00AF0585 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 43 43 0BBF20392F2D46AF00AF0585 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 44 + 0BCE60712F328E43000A3DCF /* widgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = widgetExtension.entitlements; sourceTree = "<group>"; }; 44 45 /* End PBXFileReference section */ 45 46 46 47 /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ ··· 97 98 0BBF20142F2D443D00AF0585 = { 98 99 isa = PBXGroup; 99 100 children = ( 101 + 0BCE60712F328E43000A3DCF /* widgetExtension.entitlements */, 100 102 0BBF201F2F2D443D00AF0585 /* wasup-chucks */, 101 103 0BBF203B2F2D46AF00AF0585 /* widget */, 102 104 0BSHARED12F2D500000AF0585 /* Shared */, ··· 382 384 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 385 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 384 386 ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 387 + CODE_SIGN_ENTITLEMENTS = "wasup-chucks/wasup-chucks.entitlements"; 385 388 CODE_SIGN_STYLE = Automatic; 386 - CURRENT_PROJECT_VERSION = 2; 389 + CURRENT_PROJECT_VERSION = 3; 387 390 DEVELOPMENT_TEAM = M67B42LX8D; 388 391 ENABLE_PREVIEWS = YES; 389 392 GENERATE_INFOPLIST_FILE = YES; ··· 417 420 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 418 421 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 419 422 ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 423 + CODE_SIGN_ENTITLEMENTS = "wasup-chucks/wasup-chucks.entitlements"; 420 424 CODE_SIGN_STYLE = Automatic; 421 - CURRENT_PROJECT_VERSION = 2; 425 + CURRENT_PROJECT_VERSION = 3; 422 426 DEVELOPMENT_TEAM = M67B42LX8D; 423 427 ENABLE_PREVIEWS = YES; 424 428 GENERATE_INFOPLIST_FILE = YES; ··· 451 455 buildSettings = { 452 456 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 453 457 ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 458 + CODE_SIGN_ENTITLEMENTS = widgetExtension.entitlements; 454 459 CODE_SIGN_STYLE = Automatic; 455 - CURRENT_PROJECT_VERSION = 2; 460 + CURRENT_PROJECT_VERSION = 3; 456 461 DEVELOPMENT_TEAM = M67B42LX8D; 457 462 GENERATE_INFOPLIST_FILE = YES; 458 463 INFOPLIST_FILE = widget/Info.plist; ··· 481 486 buildSettings = { 482 487 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 483 488 ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 489 + CODE_SIGN_ENTITLEMENTS = widgetExtension.entitlements; 484 490 CODE_SIGN_STYLE = Automatic; 485 - CURRENT_PROJECT_VERSION = 2; 491 + CURRENT_PROJECT_VERSION = 3; 486 492 DEVELOPMENT_TEAM = M67B42LX8D; 487 493 GENERATE_INFOPLIST_FILE = YES; 488 494 INFOPLIST_FILE = widget/Info.plist;
+1
ios/wasup-chucks/ContentView.swift
··· 87 87 await loadMenu() 88 88 } 89 89 .refreshable { 90 + await ChucksService.shared.invalidateCache() 90 91 await loadMenu() 91 92 } 92 93 .sheet(item: $selectedMeal) { meal in
+10
ios/wasup-chucks/wasup-chucks.entitlements
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>com.apple.security.application-groups</key> 6 + <array> 7 + <string>group.sh.dunkirk.sh.wasup-chucks</string> 8 + </array> 9 + </dict> 10 + </plist>
+10
ios/widgetExtension.entitlements
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>com.apple.security.application-groups</key> 6 + <array> 7 + <string>group.sh.dunkirk.sh.wasup-chucks</string> 8 + </array> 9 + </dict> 10 + </plist>