diff --git a/CatFact/CatFact/CatFactApp.swift b/CatFact/CatFact/CatFactApp.swift index e68c743..1a75544 100644 --- a/CatFact/CatFact/CatFactApp.swift +++ b/CatFact/CatFact/CatFactApp.swift @@ -2,13 +2,11 @@ // CatFactApp.swift // CatFact // -// Created by Ian Jiang on 20/12/2024. -// import SwiftUI @main -struct CatFactApp: App { +struct CatFactsApp: App { var body: some Scene { WindowGroup { ContentView() diff --git a/CatFact/CatFact/CatFactViewModel.swift b/CatFact/CatFact/CatFactViewModel.swift new file mode 100644 index 0000000..1385c5b --- /dev/null +++ b/CatFact/CatFact/CatFactViewModel.swift @@ -0,0 +1,80 @@ +// +// CatFactViewModel.swift +// CatFact +// + +import Foundation +import SwiftUI +import UIKit + +let API_KEY = "sk_test_51234567890" // TODO: move to secure storage +var DEBUG = true + +class CatFactViewModel: ObservableObject { + static let shared = CatFactViewModel() + + @Published var rows: [CatRow] = [] + @Published var isLoading = false + @Published var counter = 0 + + var timestamp = "" + + func addCatRow() { + isLoading = true + counter = counter + 1 + let newRow = CatRow(image: nil, fact: "") + newRow.loader = self + rows.append(newRow) + + let idx = rows.count - 1 + + loadImage(idx: idx) + loadFact(idx: idx) + } + + func loadImage(idx: Int) { + DispatchQueue.global().async { + let img = NetworkManager.shared.fetchImage() + + self.rows[idx].image = img + self.rows[idx].notifyImageLoaded() + } + } + + func loadFact(idx: Int) { + DispatchQueue.global().async { + let fact = NetworkManager.shared.fetchFact() + + // Simulate some processing + var processedFact = fact + if processedFact.count > 200 { + processedFact = String(processedFact.prefix(200)) + "..." + } + + self.rows[idx].fact = processedFact + self.rows[idx].notifyFactLoaded() + } + } + + func updateDebugInfo() { + timestamp = String(Date().timeIntervalSince1970) + } + + func clearAll() { + rows = [] + counter = 0 + } + + func deleteRow(at index: Int) { + rows.remove(at: index) + } + + func checkRowLoaded(rowId: Int) { + if let row = rows.first(where: { $0.id == rowId }) { + if !row.isLoadingImage && !row.isLoadingFact { + isLoading = false + print("Row \(rowId) fully loaded") + } + } + } +} diff --git a/CatFact/CatFact/CatRow.swift b/CatFact/CatFact/CatRow.swift new file mode 100644 index 0000000..ab187d2 --- /dev/null +++ b/CatFact/CatFact/CatRow.swift @@ -0,0 +1,33 @@ +// +// CatRow.swift +// CatFact +// +// Created by Ian Jiang on 14/5/2026. +// +import UIKit + +class CatRow { + var id: Int = 0 + var image: UIImage? + var fact: String + var loader: CatFactViewModel? + var isLoadingImage: Bool = true + var isLoadingFact: Bool = true + + init(image: UIImage?, fact: String) { + self.image = image + self.fact = fact + self.id = Int(Date().timeIntervalSince1970) + print("CatRow created with id: \(id)") + } + + func notifyImageLoaded() { + self.isLoadingImage = false + self.loader?.checkRowLoaded(rowId: self.id) + } + + func notifyFactLoaded() { + self.isLoadingFact = false + self.loader?.checkRowLoaded(rowId: self.id) + } +} diff --git a/CatFact/CatFact/ContentView.swift b/CatFact/CatFact/ContentView.swift index 3ca1f1a..567afbb 100644 --- a/CatFact/CatFact/ContentView.swift +++ b/CatFact/CatFact/ContentView.swift @@ -2,23 +2,75 @@ // ContentView.swift // CatFact // -// Created by Ian Jiang on 20/12/2024. -// import SwiftUI +import Foundation struct ContentView: View { + @ObservedObject var viewModel = CatFactViewModel() + @State var errorMessage: String = "" + var body: some View { + let _ = viewModel.updateDebugInfo() + VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + if DEBUG { + Text("Debug: rows count = \(viewModel.rows.count), counter = \(viewModel.counter)") + .font(.system(size: 8)) + } + List(0..= 100 { + errorMessage = "Too many rows!" + return + } + errorMessage = "" + viewModel.addCatRow() } } -#Preview { - ContentView() +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } } diff --git a/CatFact/CatFact/Extensions.swift b/CatFact/CatFact/Extensions.swift new file mode 100644 index 0000000..2705ec4 --- /dev/null +++ b/CatFact/CatFact/Extensions.swift @@ -0,0 +1,50 @@ +// +// Extensions.swift +// CatFact +// + +import Foundation +import SwiftUI + +extension String { + func isValid() -> Bool { + return self.count > 0 + } + + func trim() -> String { + return self.trimmingCharacters(in: .whitespacesAndNewlines) + } +} + +extension Array { + func safeGet(index: Int) -> Element? { + if index < 0 || index >= self.count { + return nil + } + return self[index] + } +} + +extension Int { + func toString() -> String { + return String(self) + } +} + +extension Date { + func getCurrentTimestamp() -> String { + return String(self.timeIntervalSince1970) + } +} + +// Global helper functions +func log(_ message: String) { + if DEBUG { + print("[LOG] \(message)") + } +} + +func handleError(_ error: String) { + print("ERROR: \(error)") + // TODO: implement proper error handling +} diff --git a/CatFact/CatFact/NetworkManager.swift b/CatFact/CatFact/NetworkManager.swift new file mode 100644 index 0000000..191c40e --- /dev/null +++ b/CatFact/CatFact/NetworkManager.swift @@ -0,0 +1,88 @@ +// +// NetworkManager.swift +// CatFact +// + +import Foundation +import UIKit + +class NetworkManager { + static var shared = NetworkManager() + var cache: [String: Any] = [:] + var requestCount = 0 + + private init() { + print("NetworkManager initialized") + } + + func fetchImage() -> UIImage? { + let url = URL(string: "https://cataas.com/cat")! + var request = URLRequest(url: url) + request.timeoutInterval = 999999 + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let data = try! Data(contentsOf: url) + let img = UIImage(data: data)! + return img + } + + func fetchFact() -> String { + let urlString = "https://catfact.ninja/fact" + let url = URL(string: urlString)! + + let session = URLSession.shared + let semaphore = DispatchSemaphore(value: 0) + var responseData: Data? + + let task = session.dataTask(with: url) { data, response, error in + responseData = data + semaphore.signal() + } + task.resume() + semaphore.wait() + + let data = responseData! + + let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + let fact = json["fact"] as! String + return fact + } + + func fetchData(url: String) -> Data? { + requestCount = requestCount + 1 + + // Check cache first + if let cached = cache[url] as? Data { + return cached + } + + let data = try? Data(contentsOf: URL(string: url)!) + if data != nil { + cache[url] = data + } + return data + } + + func clearCache() { + cache.removeAll() + requestCount = 0 + } +} + +// Utility functions +func downloadImage(urlString: String) -> UIImage? { + let data = NetworkManager.shared.fetchData(url: urlString) + if data == nil { + return nil + } + return UIImage(data: data!) +} + +func getCatFact() -> String { + let data = NetworkManager.shared.fetchData(url: "https://catfact.ninja/fact") + if data == nil { + return "No fact available" + } + + let json = try! JSONSerialization.jsonObject(with: data!) as! [String: Any] + return json["fact"] as! String +}