Home > other >  Having Trouble with decoding Nested JSON Response in API call with SwiftUI
Having Trouble with decoding Nested JSON Response in API call with SwiftUI

Time:07-30

I'm currently trying to utilize the NewsApi api to allow me to retrieve certain articles. I currently do have a model for which I generated

import Foundation

// MARK: - Welcome
struct NewsResponse: Codable {
    let status: String
    let totalResults: Int
    let articles: [Article]

    enum CodingKeys: String, CodingKey {
        case status = "status"
        case totalResults = "totalResults"
        case articles = "articles"
    }
}

// MARK: - Article
struct Article: Codable, Identifiable {
    let id = UUID()
    let source: Source
    let author: String?
    let title: String
    let articleDescription: String
    let url: String
    let urlToImage: String?
    let publishedAt: Date
    let content: String

    enum CodingKeys: String, CodingKey {
        case source = "source"
        case author = "author"
        case title = "title"
        case articleDescription = "description"
        case url = "url"
        case urlToImage = "urlToImage"
        case publishedAt = "publishedAt"
        case content = "content"
    }
}

// MARK: - Source
struct Source: Codable {
    let id: String?
    let name: String

    enum CodingKeys: String, CodingKey {
        case id = "id"
        case name = "name"
    }
}

I also do have my Webservice that decodes the API link which is at let pokeNews = URL(string:"https://newsapi.org/v2/everything?q=pokemon&apiKey=*APIKey*")!

The webservice code is here and I had used a Mohammed Azam tutorial for that: https://www.youtube.com/watch?v=YOCZuZz4vAw

import Foundation

class NewsWebService: ObservableObject{
    
    @Published var news: [Article] = []
    
    func getNews() async throws{
        let (data, _) = try await URLSession.shared.data(from: Constants.url.pokeNews)
        
        Task{@MainActor in
            self.news = try JSONDecoder().decode([Article].self, from: data)
        }
        
    }
    
}

Now the main issue I'm having is that I'm at a loss of understanding at how I would access the articles array from the NewsResponse model and retrieve the article.title, etc. I've attempted it multiple times, but am not quite sure how to proceed. I've looked at other tutorials, but I feel that I'm still not gaining a sense of understanding at how to effectively call apis in swift. The code in which I'm attempting to call the API within is here:

import SwiftUI

struct NewsListView: View {
    @EnvironmentObject var newsWebService: NewsWebService
    var body: some View {
        List(newsWebService.news){ article in
            Text(article.title)
        }
        
        
        HStack(alignment: .center, spacing: 16, content: {
            //Article Image
            
            Image("yak0")
                .resizable()
                .scaledToFill()
                .frame(width: 50, height: 50)
                .clipShape(RoundedRectangle(cornerRadius: 12))
            
            //Article Title
            VStack(alignment: .leading, spacing: 8){
                Text("Massive News Surrounding Pokemon")
                    .font(.title2)
                    .fontWeight(.heavy)
                    .foregroundColor(.red)
                
                //Article Source
                Text("CNN").font(.footnote)
                        .fontWeight(.bold)
                        .multilineTextAlignment(.leading)
                        .foregroundColor(.yellow)
            }
        }).onTapGesture {
            //go to webview
        }
    }
}


struct NewsListView_Previews: PreviewProvider {
    static var previews: some View {
        NewsListView()
            .previewLayout(.sizeThatFits)
            .padding()
            .environmentObject(NewsWebService())

    }
}

Any help would be appreciated. Thank you so much!

Edited for NewsResponse portion

import Foundation

class NewsWebService: ObservableObject{
    
    @Published var news: NewsResponse?
    
    func getNews() async throws{
        let (data, _) = try await URLSession.shared.data(from: Constants.url.pokeNews)
        
        Task{@MainActor in
            self.news = try JSONDecoder().decode(NewsResponse.self, from: data)
        }
        
    }
    
}

The Line showing the error: Initializer 'init(_:rowContent:)' requires that 'NewsResponse' conform to 'RandomAccessCollection'

List(newsWebService.news!){ article in
    Text(article.title)
}
    

CodePudding user response:

try this example code (note the mods in Article), works well for me. Note, do not show your secret api key, remove it:

struct ContentView: View {
    @StateObject var newsWebService = NewsWebService()
    var body: some View {
        NewsListView().environmentObject(newsWebService)
    }
}

struct NewsListView: View {
    @EnvironmentObject var newsWebService: NewsWebService
    
    var body: some View {
        List(newsWebService.news?.articles ?? []){ article in  // <-- here
            Text(article.title)
        }
        .task {
            do{
                try await newsWebService.getNews()
            } catch{
                print("---> task error: \(error)")
            }
        }
    }
}

class NewsWebService: ObservableObject{
    
    @Published var news: NewsResponse?
    
    func getNews() async throws {
            let (data, _) = try await URLSession.shared.data(from: Constants.url.pokeNews)
            Task{@MainActor in
                self.news = try JSONDecoder().decode(NewsResponse.self, from: data)
            }
    }
}

struct NewsResponse: Codable {
    let status: String
    let totalResults: Int
    let articles: [Article]
}

struct Article: Codable, Identifiable {
    let id = UUID()
    let source: Source
    let author: String?  
    let title, articleDescription: String
    let url: String
    let urlToImage: String? 
    let publishedAt: String? // <-- here
    let content: String
    
    enum CodingKeys: String, CodingKey {
        case source, author, title
        case articleDescription = "description"
        case url, urlToImage, publishedAt, content
    }
}

struct Source: Codable {
    let id: String?
    let name: String
}

This is assuming Constants.url.pokeNews is the url for "https://newsapi.org/v2/everything?q=pokemon&apiKey=YOURKEY"

EDIT-1:

If you want to use Article, let publishedAt: Date?, instead of String?, then use:

 func getNews() async throws {
     let (data, _) = try await URLSession.shared.data(from: Constants.url.pokeNews)
     Task{@MainActor in
         let decoder = JSONDecoder() // <-- here
         decoder.dateDecodingStrategy = .iso8601  // <-- here
         self.news = try decoder.decode(NewsResponse.self, from: data)
     }
 }
  • Related