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?
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]
}