jsonswiftswiftuiurlsession

JSON Data not printing to the Xcode console and won´t display on one of my tab views


I am trying to decode a JSON file from a URL, but I am having issues initially.

Error: Error decoding JSON: keyNotFound(CodingKeys(stringValue: "jobs", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "jobs", intValue: nil) ("jobs").", underlyingError: nil))

My code is below.

import SwiftUI
import SDWebImageSwiftUI

struct ContentViewJobs: View {
    var body: some View {
        ProductListView()
    }
}

struct Job: Identifiable, Codable {
    let id: Int
    let title, description: String
    let isRemote, isActive: Bool
    let locations: [Int]
}

class DataManager: ObservableObject {
    struct Returned: Decodable {
        let jobs: [Job]
    }

    @Published var jobsArray: [Job] = []
    @Published var filteredJobs: [Job] = []
    
    let apiKey = "MyAPIKey"

    
    func fetchData(completion: @escaping () -> Void) {
            guard let url = URL(string: "https://api.forager.ai/v1/job_search/") else {
                print("Invalid URL")
                completion()
                return
            }

            var request = URLRequest(url: url)
            request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")

            URLSession.shared.dataTask(with: request) { data, _, error in
                if let error = error {
                    print("Network error: \(error)")
                    completion()
                    return
                }

                if let data = data {
                    do {
                        let decodedData = try JSONDecoder().decode(Returned.self, from: data)
                        DispatchQueue.main.async {
                            self.jobsArray = decodedData.jobs
                            self.filteredJobs = self.jobsArray
                        }
                        completion()
                    } catch {
                        print("Error decoding JSON: \(error)")
                        completion()
                    }
                }
            }.resume()
        }
    }

struct ProductListView: View {
    @StateObject private var dataManager = DataManager()
    @State private var searchText: String = ""

    
    var body: some View {
        NavigationView {
            if dataManager.jobsArray.isEmpty {
                Text("Loading Data...")
            } else {
                VStack {
                    SearchBar(text: $searchText, placeholder: "Search Jobs")
                        .padding(.bottom, 20)
                        .padding(.top, 14)
                        .padding(.horizontal, 22)
                        .background(Color.white)

                    
                    List(0..<dataManager.filteredJobs.count) { index in
                        let job = dataManager.filteredJobs[index]
                        NavigationLink(destination: ProductDetailView(Job: job)) {
                            VStack(alignment: .leading, spacing: 8) {
//                                WebImage(url: URL(string: product.thumbnail))
//                                    .resizable()
//                                    .scaledToFit()
//                                    .frame(height: 150)
//                                    .cornerRadius(8)

                                Text(job.title)  // Use 'job.title' instead of 'Job.title'
                                    .font(.title2)
                                    .foregroundColor(.pink)
                                    .padding(4)

                                
                                Text(job.description)
                                    .font(.callout)
                                    .foregroundColor(.primary)
                                    .padding(4)
                                
//                                Text(product.category)
//                                    .font(.headline)
//                                    .foregroundColor(.secondary)
//                                    .padding(4)

                                
                                Text(job.isRemote ? "Remote" : "Not Remote")
                                    .font(.headline)
//                                    .bold()
                                    .foregroundColor(.secondary)
                                    .padding(4)



//                                Text("brand: $\(String(format: "%.2f", product.brand))")
//                                    .font(.subheadline)
//                                    .foregroundColor(.secondary)
                            }
                            .padding(12) // Add padding around the entire VStack
//                            .background(Color.white)
                            .cornerRadius(8)

                        }

                    }
                    .listStyle(PlainListStyle()) // or .listStyle(InsetGroupedListStyle())


                }

            }

        }

        .onAppear {
            dataManager.fetchData {
                print("Products Array: \(dataManager.jobsArray)")
            }
        }
        .onChange(of: searchText) { newValue in
            if !searchText.isEmpty {
                dataManager.filteredJobs = dataManager.jobsArray.filter {
                    $0.title.lowercased().contains(searchText.lowercased())
                }
            } else {
                dataManager.filteredJobs = dataManager.jobsArray
            }
        }
    }

}

struct ProductDetailView: View {
    let Job: Job
    

    //This view is presented after a Job cell is slected
    var body: some View {
        Text("Job Detail View")
        Text(Job.title)
    }
}

struct Returned: Decodable {
    let jobs: [Job]

    enum CodingKeys: String, CodingKey {
        case jobs
    }
}


struct SearchBar: View {
    @Binding var text: String
    var placeholder: String

    var body: some View {
        HStack {
            TextField(placeholder, text: $text)
                .padding(.horizontal, 25)
                .padding(.vertical, 12)
                .background(Color(.systemGray6))
                .cornerRadius(8)
                .padding(.trailing, 0)
                .foregroundColor(.black)  // Set the text color

                .onTapGesture {
                    UIApplication.shared.sendAction(#selector(UIResponder.becomeFirstResponder), to: nil, from: nil, for: nil)
                }
        }
    }
}

The forager JSON format looks like this:

{ "page": 0, "job_source": "linkedin", "date_featured_start": "2019-08-24", "date_featured_end": "2019-08-24", "organization_ids": [ 0 ], "title": "string", "description": "string", "is_remote": true, "is_active": true, "locations": [ 0 ] }

I have tried debugging the code with print statements at all places in the code and debugging. My iOS simulator displays the tab titled "No products available."

Where am I getting wrong?


Solution

  • Try this approach, where you have the .onAppear{...} attached to the NavigationStack (note NavigationView is deprecated). Also decode the json data using a valid Returned model and using @StateObject for your dataManager.

    Works well for me, using MacOS 14.2, with Xcode 15.1, tested on real ios17 devices and MacCatalyst.

    You will need to consult the docs of the API to determine if any of the properties of Returned and Product are optional, and add ? to those.

    Full working code:

    struct ContentView: View {
        var body: some View {
            ProductListView()
        }
    }
    
    class DataManager: ObservableObject {
        
        @Published var productsArray: [Product] = []  // <--- here
    
        func fetchData(completion: @escaping () -> Void) {
            guard let url = URL(string: "https://compassjobsapp.net/DummyJSONData.json") else {
                print("Invalid URL")
                completion()
                return
            }
    
            URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    print("Network error: \(error)")
                    completion()
                    return
                }
    
                if let data = data {
                        print("Received Data: \(String(data: data, encoding: .utf8) ?? "Unable   to convert data to string")")
                    do {
                        let decodedData = try JSONDecoder().decode(Returned.self, from: data)
                        DispatchQueue.main.async {
                            self.productsArray = decodedData.products  // <--- here `s`
                            print("Decoded Data: \(decodedData)")
                            print("Products Array: \(self.productsArray)")
                        }
                        print("Decoded Data: \(decodedData)")
                        completion()
                    } catch {
                        print("Error decoding JSON: \(error)")
                        completion()
                            
                    }
                }
            }.resume()
        }
    }
    
    struct ProductListView: View {
        @StateObject private var dataManager = DataManager()  // <--- here
    
        var body: some View {
            NavigationStack {  // <--- here
                if dataManager.productsArray.isEmpty {
                    Text("No products available")
                } else {
                    List(dataManager.productsArray) { products in
                        NavigationLink(destination: ProductDetailView(product: products)) {
                            VStack(alignment: .leading, spacing: 8) {
                                // commented for my testing
    //                            WebImage(url: URL(string: products.thumbnail))
    //                                .resizable()
    //                                .scaledToFit()
    //                                .frame(height: 150)
    //                                .cornerRadius(8)
    
                                Text(products.title)
                                    .font(.headline)
                                    .foregroundColor(.primary)
    
                                Text("Price: $\(String(format: "%.2f", products.price))")
                                    .font(.subheadline)
                                    .foregroundColor(.secondary)
                            }
                            .padding(10)
                        }
                    }
                    .navigationTitle("Products")
                }
            }
            .onAppear {  // <--- here
                dataManager.fetchData {
                    print("Products Array: \(dataManager.productsArray)")
                    // Print the products array to the console
                    print(dataManager.productsArray)
                }
            }
        }
    }
    
    struct ProductDetailView: View {
        let product: Product  // <--- here
    
        var body: some View {
            Text("Product Detail View")
            Text(product.title)
        }
    }
    
    struct Returned: Codable {
        let products: [Product]   // <--- here, note `s`
        let total, skip, limit: Int
    }
    
    struct Product: Identifiable, Codable {  // <--- here
        let id: Int
        let title, description: String
        let price: Int
        let discountPercentage, rating: Double
        let stock: Int
        let brand, category: String
        let thumbnail: String
        let images: [String]
    }