Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions CatFact/CatFact/CatFactApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
80 changes: 80 additions & 0 deletions CatFact/CatFact/CatFactViewModel.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
}
33 changes: 33 additions & 0 deletions CatFact/CatFact/CatRow.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
70 changes: 61 additions & 9 deletions CatFact/CatFact/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<viewModel.rows.count, id: \.self) { i in
HStack {
if viewModel.rows[i].image != nil {
Image(uiImage: viewModel.rows[i].image!)
.resizable()
.frame(width: 100, height: 100)
} else {
Rectangle().fill(Color.gray).frame(width: 100, height: 100)
}
VStack(alignment: .leading) {
Text(viewModel.rows[i].fact)
if DEBUG {
Text("ID: \(viewModel.rows[i].id)")
.font(.caption)
.foregroundColor(.gray)
}
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
viewModel.deleteRow(at: i)
viewModel.counter = viewModel.counter - 1
} label: {
Label("Delete", systemImage: "trash")
}
}
}
.id(viewModel.counter)
if errorMessage != "" {
Text(errorMessage)
.foregroundColor(.red)
}
Button("Add Cat Row") {
addCatRow()
}
}
.onAppear {
addCatRow()
}
.padding()
}

func addCatRow() {
if viewModel.counter >= 100 {
errorMessage = "Too many rows!"
return
}
errorMessage = ""
viewModel.addCatRow()
}
}

#Preview {
ContentView()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
50 changes: 50 additions & 0 deletions CatFact/CatFact/Extensions.swift
Original file line number Diff line number Diff line change
@@ -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
}
88 changes: 88 additions & 0 deletions CatFact/CatFact/NetworkManager.swift
Original file line number Diff line number Diff line change
@@ -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
}