From d88bf0783759c5b7ba77a27efb19f50f29943097 Mon Sep 17 00:00:00 2001 From: Ian Jiang Date: Fri, 20 Dec 2024 09:48:17 +1100 Subject: [PATCH 1/4] show cat fact --- CatFact/CatFact/CatFactApp.swift | 4 +- CatFact/CatFact/ContentView.swift | 198 +++++++++++++++++++++++++-- CatFact/CatFact/Extensions.swift | 50 +++++++ CatFact/CatFact/NetworkManager.swift | 56 ++++++++ 4 files changed, 294 insertions(+), 14 deletions(-) create mode 100644 CatFact/CatFact/Extensions.swift create mode 100644 CatFact/CatFact/NetworkManager.swift 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/ContentView.swift b/CatFact/CatFact/ContentView.swift index 3ca1f1a..573927c 100644 --- a/CatFact/CatFact/ContentView.swift +++ b/CatFact/CatFact/ContentView.swift @@ -1,24 +1,200 @@ // -// ContentView.swift -// CatFact -// -// Created by Ian Jiang on 20/12/2024. +// CatFactApp.swift +// ContentView // import SwiftUI +import Foundation +import UIKit + +var g_data: [CatRow] = [] +var temp = "" +var x = 0 +let API_KEY = "sk_test_51234567890" // TODO: move to secure storage +var DEBUG = true + +class CatRow { + var image: UIImage? + var fact: String + var data: Data? + var loader: ContentView? + var timestamp: String = "" + var id: Int = 0 + 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) + } + + deinit { + print("CatRow destroyed") + } +} struct ContentView: View { + @State var rows: [CatRow] = [] + @State var isLoading = false + @State var counter = 0 + @State var errorMessage: String = "" + var body: some View { VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + if DEBUG { + Text("Debug: rows count = \(rows.count), counter = \(counter)") + .font(.system(size: 8)) + } + ScrollView { + VStack { + ForEach(0..= 100 { + errorMessage = "Too many rows!" + return + } + + isLoading = true + counter = counter + 1 + var newRow = CatRow(image: nil, fact: "") + newRow.loader = self + rows.append(newRow) + g_data = rows + let idx = rows.count - 1 + temp = "loading..." + x = x + 1 + + print("Adding row at index: \(idx)") + + // Load image + DispatchQueue.global().async { + 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)! + + // Add artificial delay to simulate slow network + Thread.sleep(forTimeInterval: Double(arc4random_uniform(3))) + + self.rows[idx].image = img + self.rows[idx].data = data + self.rows[idx].timestamp = String(Date().timeIntervalSince1970) + self.rows[idx].notifyImageLoaded() + + if DEBUG { + print("Image loaded for index: \(idx), size: \(data.count) bytes") + } + } + + // Load fact + DispatchQueue.global().async { + sleep(2) + let urlString = "https://catfact.ninja/fact" + let url = URL(string: urlString)! + let data = try! Data(contentsOf: url) + + let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + let fact = json["fact"] as! String + + // Simulate some processing + var processedFact = fact + if processedFact.count > 200 { + processedFact = String(processedFact.prefix(200)) + "..." + } + + self.rows[idx].fact = processedFact + self.rows[idx].notifyFactLoaded() + temp = "" + errorMessage = "" + + if DEBUG { + print("Fact loaded for index: \(idx)") + } + + // Save to global state + g_data = self.rows + } + } + + func clearAll() { + rows = [] + counter = 0 + g_data = [] + } + + func checkRowLoaded(rowId: Int) { + // Find the row by id + if let row = rows.first(where: { $0.id == rowId }) { + if !row.isLoadingImage && !row.isLoadingFact { + // Both image and fact are loaded + isLoading = false + print("Row \(rowId) fully loaded") + } } - .padding() } } -#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..988ed0e --- /dev/null +++ b/CatFact/CatFact/NetworkManager.swift @@ -0,0 +1,56 @@ +// +// 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 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 +} From 1ebb6d81c11cd1e8c84ac655e699ed5c1c4756e5 Mon Sep 17 00:00:00 2001 From: Ian Jiang Date: Tue, 5 May 2026 15:26:48 +1000 Subject: [PATCH 2/4] vm --- CatFact/CatFact/CatFactViewModel.swift | 152 ++++++++++++++++++++ CatFact/CatFact/ContentView.swift | 190 +++++-------------------- 2 files changed, 185 insertions(+), 157 deletions(-) create mode 100644 CatFact/CatFact/CatFactViewModel.swift diff --git a/CatFact/CatFact/CatFactViewModel.swift b/CatFact/CatFact/CatFactViewModel.swift new file mode 100644 index 0000000..20adfe2 --- /dev/null +++ b/CatFact/CatFact/CatFactViewModel.swift @@ -0,0 +1,152 @@ +// +// CatFactViewModel.swift +// CatFact +// + +import Foundation +import SwiftUI +import UIKit + +var g_data: [CatRow] = [] +var temp = "" +let API_KEY = "sk_test_51234567890" // TODO: move to secure storage +var DEBUG = true + +class CatRow { + var image: UIImage? + var fact: String + var data: Data? + var loader: CatFactViewModel? + var timestamp: String = "" + var id: Int = 0 + 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) + } + + deinit { + print("CatRow destroyed") + } +} + +class CatFactViewModel: ObservableObject { + static let shared = CatFactViewModel() + + @Published var rows: [CatRow] = [] + @Published var isLoading = false + @Published var counter = 0 + + func addCatRow() { + isLoading = true + counter = counter + 1 + var newRow = CatRow(image: nil, fact: "") + newRow.loader = self + rows.append(newRow) + g_data = rows + let idx = rows.count - 1 + temp = "loading..." + + print("Adding row at index: \(idx)") + + // Load image + DispatchQueue.global().async { + 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)! + + // Add artificial delay to simulate slow network + Thread.sleep(forTimeInterval: Double(arc4random_uniform(3))) + + self.rows[idx].image = img + self.rows[idx].data = data + self.rows[idx].timestamp = String(Date().timeIntervalSince1970) + self.rows[idx].notifyImageLoaded() + + if DEBUG { + print("Image loaded for index: \(idx), size: \(data.count) bytes") + } + } + + // Load fact + DispatchQueue.global().async { + sleep(2) + 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 + + // Simulate some processing + var processedFact = fact + if processedFact.count > 200 { + processedFact = String(processedFact.prefix(200)) + "..." + } + + self.rows[idx].fact = processedFact + self.rows[idx].notifyFactLoaded() + temp = "" + + if DEBUG { + print("Fact loaded for index: \(idx)") + } + + // Save to global state + g_data = self.rows + } + } + + func updateDebugInfo() { + g_data = rows + print("Debug: Updated with \(rows.count) rows") + } + + func clearAll() { + rows = [] + counter = 0 + g_data = [] + } + + 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/ContentView.swift b/CatFact/CatFact/ContentView.swift index 573927c..567afbb 100644 --- a/CatFact/CatFact/ContentView.swift +++ b/CatFact/CatFact/ContentView.swift @@ -1,195 +1,71 @@ // -// CatFactApp.swift -// ContentView +// ContentView.swift +// CatFact // import SwiftUI import Foundation -import UIKit - -var g_data: [CatRow] = [] -var temp = "" -var x = 0 -let API_KEY = "sk_test_51234567890" // TODO: move to secure storage -var DEBUG = true - -class CatRow { - var image: UIImage? - var fact: String - var data: Data? - var loader: ContentView? - var timestamp: String = "" - var id: Int = 0 - 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) - } - - deinit { - print("CatRow destroyed") - } -} struct ContentView: View { - @State var rows: [CatRow] = [] - @State var isLoading = false - @State var counter = 0 + @ObservedObject var viewModel = CatFactViewModel() @State var errorMessage: String = "" var body: some View { + let _ = viewModel.updateDebugInfo() + VStack { if DEBUG { - Text("Debug: rows count = \(rows.count), counter = \(counter)") + Text("Debug: rows count = \(viewModel.rows.count), counter = \(viewModel.counter)") .font(.system(size: 8)) } - ScrollView { - VStack { - ForEach(0..= 100 { + if viewModel.counter >= 100 { errorMessage = "Too many rows!" return } - - isLoading = true - counter = counter + 1 - var newRow = CatRow(image: nil, fact: "") - newRow.loader = self - rows.append(newRow) - g_data = rows - let idx = rows.count - 1 - temp = "loading..." - x = x + 1 - - print("Adding row at index: \(idx)") - - // Load image - DispatchQueue.global().async { - 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)! - - // Add artificial delay to simulate slow network - Thread.sleep(forTimeInterval: Double(arc4random_uniform(3))) - - self.rows[idx].image = img - self.rows[idx].data = data - self.rows[idx].timestamp = String(Date().timeIntervalSince1970) - self.rows[idx].notifyImageLoaded() - - if DEBUG { - print("Image loaded for index: \(idx), size: \(data.count) bytes") - } - } - - // Load fact - DispatchQueue.global().async { - sleep(2) - let urlString = "https://catfact.ninja/fact" - let url = URL(string: urlString)! - let data = try! Data(contentsOf: url) - - let json = try! JSONSerialization.jsonObject(with: data) as! [String: Any] - let fact = json["fact"] as! String - - // Simulate some processing - var processedFact = fact - if processedFact.count > 200 { - processedFact = String(processedFact.prefix(200)) + "..." - } - - self.rows[idx].fact = processedFact - self.rows[idx].notifyFactLoaded() - temp = "" - errorMessage = "" - - if DEBUG { - print("Fact loaded for index: \(idx)") - } - - // Save to global state - g_data = self.rows - } - } - - func clearAll() { - rows = [] - counter = 0 - g_data = [] - } - - func checkRowLoaded(rowId: Int) { - // Find the row by id - if let row = rows.first(where: { $0.id == rowId }) { - if !row.isLoadingImage && !row.isLoadingFact { - // Both image and fact are loaded - isLoading = false - print("Row \(rowId) fully loaded") - } - } + errorMessage = "" + viewModel.addCatRow() } } From f42e8d4a96b21680020cc65da8a61c3a1d834b2f Mon Sep 17 00:00:00 2001 From: Ian Jiang Date: Tue, 12 May 2026 10:25:51 +1000 Subject: [PATCH 3/4] Clean up --- CatFact/CatFact/CatFactViewModel.swift | 29 +++++--------------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/CatFact/CatFact/CatFactViewModel.swift b/CatFact/CatFact/CatFactViewModel.swift index 20adfe2..caf6fe4 100644 --- a/CatFact/CatFact/CatFactViewModel.swift +++ b/CatFact/CatFact/CatFactViewModel.swift @@ -7,18 +7,14 @@ import Foundation import SwiftUI import UIKit -var g_data: [CatRow] = [] -var temp = "" let API_KEY = "sk_test_51234567890" // TODO: move to secure storage var DEBUG = true class CatRow { + var id: Int = 0 var image: UIImage? var fact: String - var data: Data? var loader: CatFactViewModel? - var timestamp: String = "" - var id: Int = 0 var isLoadingImage: Bool = true var isLoadingFact: Bool = true @@ -38,10 +34,6 @@ class CatRow { self.isLoadingFact = false self.loader?.checkRowLoaded(rowId: self.id) } - - deinit { - print("CatRow destroyed") - } } class CatFactViewModel: ObservableObject { @@ -51,15 +43,15 @@ class CatFactViewModel: ObservableObject { @Published var isLoading = false @Published var counter = 0 + var timestamp = "" + func addCatRow() { isLoading = true counter = counter + 1 - var newRow = CatRow(image: nil, fact: "") + let newRow = CatRow(image: nil, fact: "") newRow.loader = self rows.append(newRow) - g_data = rows let idx = rows.count - 1 - temp = "loading..." print("Adding row at index: \(idx)") @@ -72,12 +64,7 @@ class CatFactViewModel: ObservableObject { let data = try! Data(contentsOf: url) let img = UIImage(data: data)! - // Add artificial delay to simulate slow network - Thread.sleep(forTimeInterval: Double(arc4random_uniform(3))) - self.rows[idx].image = img - self.rows[idx].data = data - self.rows[idx].timestamp = String(Date().timeIntervalSince1970) self.rows[idx].notifyImageLoaded() if DEBUG { @@ -115,26 +102,20 @@ class CatFactViewModel: ObservableObject { self.rows[idx].fact = processedFact self.rows[idx].notifyFactLoaded() - temp = "" if DEBUG { print("Fact loaded for index: \(idx)") } - - // Save to global state - g_data = self.rows } } func updateDebugInfo() { - g_data = rows - print("Debug: Updated with \(rows.count) rows") + timestamp = String(Date().timeIntervalSince1970) } func clearAll() { rows = [] counter = 0 - g_data = [] } func deleteRow(at index: Int) { From 727117f8cbb2505dcfb2f4e8491f55c5001ab806 Mon Sep 17 00:00:00 2001 From: Ian Jiang Date: Thu, 14 May 2026 10:55:16 +1000 Subject: [PATCH 4/4] More clean up --- CatFact/CatFact/CatFactViewModel.swift | 71 ++++---------------------- CatFact/CatFact/CatRow.swift | 33 ++++++++++++ CatFact/CatFact/NetworkManager.swift | 32 ++++++++++++ 3 files changed, 74 insertions(+), 62 deletions(-) create mode 100644 CatFact/CatFact/CatRow.swift diff --git a/CatFact/CatFact/CatFactViewModel.swift b/CatFact/CatFact/CatFactViewModel.swift index caf6fe4..1385c5b 100644 --- a/CatFact/CatFact/CatFactViewModel.swift +++ b/CatFact/CatFact/CatFactViewModel.swift @@ -10,32 +10,6 @@ import UIKit let API_KEY = "sk_test_51234567890" // TODO: move to secure storage var DEBUG = true -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) - } -} - class CatFactViewModel: ObservableObject { static let shared = CatFactViewModel() @@ -51,48 +25,25 @@ class CatFactViewModel: ObservableObject { let newRow = CatRow(image: nil, fact: "") newRow.loader = self rows.append(newRow) + let idx = rows.count - 1 - print("Adding row at index: \(idx)") + loadImage(idx: idx) + loadFact(idx: idx) + } - // Load image + func loadImage(idx: Int) { DispatchQueue.global().async { - 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)! + let img = NetworkManager.shared.fetchImage() self.rows[idx].image = img self.rows[idx].notifyImageLoaded() - - if DEBUG { - print("Image loaded for index: \(idx), size: \(data.count) bytes") - } } + } - // Load fact + func loadFact(idx: Int) { DispatchQueue.global().async { - sleep(2) - 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 + let fact = NetworkManager.shared.fetchFact() // Simulate some processing var processedFact = fact @@ -102,10 +53,6 @@ class CatFactViewModel: ObservableObject { self.rows[idx].fact = processedFact self.rows[idx].notifyFactLoaded() - - if DEBUG { - print("Fact loaded for index: \(idx)") - } } } 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/NetworkManager.swift b/CatFact/CatFact/NetworkManager.swift index 988ed0e..191c40e 100644 --- a/CatFact/CatFact/NetworkManager.swift +++ b/CatFact/CatFact/NetworkManager.swift @@ -15,6 +15,38 @@ class NetworkManager { 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