如何将数据绑定到 ViewModel 以便在 MVVM 中的 UI 上显示它?

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

在我的应用程序中,我使用的是 MVVM 模式。 下面是我的模型。

    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
}

下面是我的 ViewModel。用于显示来自 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!
    }
}

下面是我的API请求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()
    }
}

收到我 API 的回复后,我想在屏幕上显示它。为此,我在下面添加了 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?
            }
        }
    }
}

请告诉我。为了在 UI.

上展示,我必须写些什么

根据 newsapi.org 的文档,您的请求将 return 一个 NewsModel 对象而不是数组。因此,将 Webservice class 更改为:

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()
    }
}

您需要将这些接收到的值映射到 NewsArticleViewModel 类型。例如:

Webservice().getTopNews { articles in
      if let articles = articles {
            self.news = articles.map{NewsArticleViewModel(article: [=11=])}
      }
}

并从 NewsArticleViewModel 结构中删除 let news: NewsModel,因为它不需要。

编辑:

好像是:

let publishedAt: Date

正在抛出错误。 Jsondecoder 无法将字符串解释为日期。改变你的 Webservice。我在我的回答中更新了它。

您可以删除遗留的 MVVM 模式并在适当的 SwiftUI 中执行此操作,如下所示:

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 = []
            }
        }
    }
}