Search code examples
swiftmvvmswiftuidata-binding

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


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 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.


Solution

  • 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.