Home > Net >  How to bind data to ViewModel for showing it on UI in MVVM?
How to bind data to ViewModel for showing it on UI in MVVM?

Time:04-12

In my app I am using MVVM pattern. Below is my Model.

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

struct Article: Codable {
    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, author, title
        case articleDescription = "description"
        case url, urlToImage, publishedAt, content
    }
}

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

Below is my ViewModel. Which is used for show the data from API.

   struct NewsArticleViewModel {
    
    let news: NewsModel
    let article: Article

    var title:String {
        return self.article.title
    }
    
    var publication:String {
        return self.article.articleDescription!
    }

    var imageURL:String {
        return self.article.urlToImage!
    }
}

Below is my API request class.

   class Webservice {
    
    func getTopNews(completion: @escaping (([NewsModel]?) -> Void)) {
        
        guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
            fatalError("URL is not correct!!!")
        }
        
        URLSession.shared.dataTask(with: url) {
            data, response, error in
            
            guard let data = data, error == nil else {
                DispatchQueue.main.async {
                    completion(nil)
                }
                return
            }
            let news = try? JSONDecoder().decode([NewsModel].self, from: data)
            DispatchQueue.main.async {
                completion(news)
            }
        }.resume()
    }
}

After receiving response from my API I want to show it on screen. For this I added below ViewModel.

    class NewsListViewModel: ObservableObject {
    
    @Published var news: [NewsArticleViewModel] = [NewsArticleViewModel]()
    
    func load() {
        fetchNews()
    }
    
    private func fetchNews() {
        Webservice().getTopNews {
            news in
            
            if let news = news {
                
//How to bind this data to NewsArticleViewModel and show it on UI?
            }
        }
    }
}

Please let me know. What I have to write there for showing it on UI.

CodePudding user response:

According to the documentation of newsapi.org your request will return one NewsModel object not an array. So change your Webservice class to:

class Webservice {
    //Change the completion handler to return an array of Article
    func getTopNews(completion: @escaping (([Article]?) -> Void)) {
        
        guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
            fatalError("URL is not correct!!!")
        }
        
        URLSession.shared.dataTask(with: url) {
            data, response, error in
            
            guard let data = data, error == nil else {
                DispatchQueue.main.async {
                    completion(nil)
                }
                return
            }
            // decode to a single NewsModel object instead of an array
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .iso8601

            let news = try? decoder.decode(NewsModel.self, from: data)
            DispatchQueue.main.async {
                // completion with an optional array of Article
                completion(news?.articles)
            }
        }.resume()
    }
}

You would need to map those received values to NewsArticleViewModel types. For example:

Webservice().getTopNews { articles in
      if let articles = articles {
            self.news = articles.map{NewsArticleViewModel(article: $0)}
      }
}

And remove let news: NewsModel from the NewsArticleViewModel struct as it is not needed.

Edit:

It seems:

let publishedAt: Date

is throwing an error. Jsondecoder fails to interpret the string to a date. Change your Webservice. I´ve updated it in my answer.

CodePudding user response:

You could remove the legacy MVVM pattern and do it in proper SwiftUI like this:

struct ContentView: View {
    @State private var articles = [Article]()

    var body: some View {
        NavigationView {
            List(articles) { article in
                Text(article.title)
            }
            .navigationTitle("Articles")
        }
        .task {
            do {
                let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb")!
                let (data, _) = try await URLSession.shared.data(from: url)
                articles = try JSONDecoder().decode([Article].self, from: data)
            } catch {
                articles = []
            }
        }
    }
}
  • Related