Swift MVVM 嵌入式网络获取图像 URL

发布于 2025-01-11 22:38:06 字数 4201 浏览 0 评论 0原文

我正在使用 MVVM 绑定快速构建一个简单的应用程序。 我的应用程序所做的只是从 url 获取数据并获取 json 响应,然后在表视图上显示信息。 每个单元格包含一个标题、一个副标题和一个图像。 输入图片此处描述

但是,图像在 json 响应中显示为字符串。因此,我需要额外的网络获取来在第一次网络调用后获取每个单元格的图像。

"articles": [
        {
            "title": "Wall Street tumbles with Nasdaq leading declines  Reuters",
            "description": "Wall Street's main indexes tumbled on Monday with Nasdaq leading the declines as technology stocks dropped on expectations of a sooner-than-expected rate hike that pushed U.S. Treasury yields to fresh two-year highs.",
            "url": "https://www.reuters.com/markets/europe/wall-street-tumbles-with-nasdaq-leading-declines-2022-01-10/",
            "urlToImage": "https://www.reuters.com/resizer/2cEiuwViTo_kOe7eWg4Igm8pm_Q=/1200x628/smart/filters:quality(80)/cloudfront-us-east-2.images.arcpublishing.com/reuters/POAM3MQFAJJX3MRXQW772WYKCA.jpg",
            
        }]

我的问题是,我应该如何修改我的代码以使 tableCellView 更加“纯粹”?目前它正在调用网络获取来获取图像。我应该将图像获取部分从 tableviewcell 配置函数移到哪里? 我应该将模型更改为包含 UIImage 但不包含字符串吗?

我的模型:

struct Articles: Codable {
    let articles: [Article]
}

struct Article: Codable {
    let title: String
    let description: String?
    let urlToImage: String?
}

我的 ViewModel:

struct ViewMode {
    var articles: Observable<[Article]> = Observable([])
}

我的 ViewController 中的主要功能:

override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self
        viewModel.articles.bind { [weak self] _ in
            DispatchQueue.main.async {
                self?.tableView.reloadData()
            }
        }
        fetchArticlesFromLocal(fileNmae: "response")
    }
func fetchArticlesFromLocal(fileNmae: String) {
        networkManager.fetchLocalJson(name: fileNmae) { [weak self] result in
            switch result {
            case.success(let data):
                guard let data = data else {return}

                do {
                    let articles = try JSONDecoder().decode(Articles.self, from: data)
                    self.viewModel.articles.value = articles.articles.compactMap({
                        Article(title: $0.title, description: $0.description, urlToImage: $0.urlToImage)
                    })
                } catch {
                    print(error.localizedDescription)
                }
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.articles.value?.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.cellID, for: indexPath) as? ImageTableViewCell {
            if let articles = viewModel.articles.value {
                cell.config(with: articles[indexPath.row])
            }
        }
        return UITableViewCell()
    }
}

我的 tableCellView:

func config(with article: Article) {
    titleView.text = article.title
    descriptionView.text = article.description
    networkManager.fetchImage(url: article.urlToImage) { [weak self] result in
        switch result {
        case .success(let image):
            DispatchQueue.main.async {
                self?.iconView.image = image
            }
        case .failure(let error):
            return
        }
    }
}

我的绑定:

class Observable<T> {
    var value: T? {
        didSet {
            listener?(value)
        }
    }

    typealias Listener = ((T?) -> Void)
    var listener: Listener?
    
    init(_ value: T?) {
        self.value = value
    }
    
    func bind(_ listener: @escaping Listener) {
        self.listener = listener
        listener(value)
    }
}

I'm building a simple app with swift by using MVVM binding.
What my app does is simply fetching data from a url and get a json response, and show the info on a table view.
Each cell contains a title, a subtitle, and an image.
enter image description here

However, the image is showing as a string in the json response. So I will need an extra network fetch to get the image for each cell after the 1st network call.

"articles": [
        {
            "title": "Wall Street tumbles with Nasdaq leading declines  Reuters",
            "description": "Wall Street's main indexes tumbled on Monday with Nasdaq leading the declines as technology stocks dropped on expectations of a sooner-than-expected rate hike that pushed U.S. Treasury yields to fresh two-year highs.",
            "url": "https://www.reuters.com/markets/europe/wall-street-tumbles-with-nasdaq-leading-declines-2022-01-10/",
            "urlToImage": "https://www.reuters.com/resizer/2cEiuwViTo_kOe7eWg4Igm8pm_Q=/1200x628/smart/filters:quality(80)/cloudfront-us-east-2.images.arcpublishing.com/reuters/POAM3MQFAJJX3MRXQW772WYKCA.jpg",
            
        }]

My question is, how should I modify my code to make the tableCellView more "pure"? Currently it's calling network fetch to get the image. Where should I move that image fetching part from the tableviewcell config function to?
Should I change my Model to to contains the UIImage but NOT the string?

My Model:

struct Articles: Codable {
    let articles: [Article]
}

struct Article: Codable {
    let title: String
    let description: String?
    let urlToImage: String?
}

My ViewModel:

struct ViewMode {
    var articles: Observable<[Article]> = Observable([])
}

Main functions in my ViewController:

override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self
        viewModel.articles.bind { [weak self] _ in
            DispatchQueue.main.async {
                self?.tableView.reloadData()
            }
        }
        fetchArticlesFromLocal(fileNmae: "response")
    }
func fetchArticlesFromLocal(fileNmae: String) {
        networkManager.fetchLocalJson(name: fileNmae) { [weak self] result in
            switch result {
            case.success(let data):
                guard let data = data else {return}

                do {
                    let articles = try JSONDecoder().decode(Articles.self, from: data)
                    self.viewModel.articles.value = articles.articles.compactMap({
                        Article(title: $0.title, description: $0.description, urlToImage: $0.urlToImage)
                    })
                } catch {
                    print(error.localizedDescription)
                }
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.articles.value?.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.cellID, for: indexPath) as? ImageTableViewCell {
            if let articles = viewModel.articles.value {
                cell.config(with: articles[indexPath.row])
            }
        }
        return UITableViewCell()
    }
}

My tableCellView:

func config(with article: Article) {
    titleView.text = article.title
    descriptionView.text = article.description
    networkManager.fetchImage(url: article.urlToImage) { [weak self] result in
        switch result {
        case .success(let image):
            DispatchQueue.main.async {
                self?.iconView.image = image
            }
        case .failure(let error):
            return
        }
    }
}

My binding:

class Observable<T> {
    var value: T? {
        didSet {
            listener?(value)
        }
    }

    typealias Listener = ((T?) -> Void)
    var listener: Listener?
    
    init(_ value: T?) {
        self.value = value
    }
    
    func bind(_ listener: @escaping Listener) {
        self.listener = listener
        listener(value)
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

晨与橙与城 2025-01-18 22:38:06

首先,你说你使用MVVM,但我在实现中看到了一些问题:你将一个模型(Article实例)传递给单元格,但它应该是一个ViewModel(ArticleViewModel实例,但你没有创建该结构),因为视图不应直接引用此架构中的模型。

您似乎还在自定义单元内添加了异步/网络代码,这也违反了架构和关注点分离(即使在 MVC 中,该代码也不应该存在,因为网络请求是服务层的一部分,而不是 UI 层) 。单元格/视图应该知道如何配置自身:1.当图像可用时和2.当图像不可用时...(例如,每当图像可用时,您就从VC调用单元格上的重新加载)

关于您的问题,添加图像下载代码的更好位置是 cellForRowAt 方法。一般来说,ViewModel 不应与 UIKit 绑定,因此为图像提供 URL 或 String 属性就可以了。
为了使 UI 在滚动时平滑显示,您可能希望在以下情况下取消请求: 1. 当图像准备好重用时(即扔进重用池中)或 2. 当它从屏幕的可见区域消失时。

这是一篇使用方法 #1 的好文章: https://www.donnywals.com/efficiently-loading-images-in-table-views-and-collection-views/

请注意,它将图像缓存在内存中,但您可能还想将它们缓存在磁盘上。

视图控制器通常也被认为更像是一个视图,因此从技术上讲,我们仍然将网络与 UI 混合在一起。也许最好使用存储库模式,或者在使用 MVC 时直接作为视图控制器的依赖项,或者作为 VC 视图模型的依赖项。视图模型不应该知道存储库是否使用 Web 请求来获取图像或将它们缓存在内存或磁盘上,因此为此使用协议和依赖注入(视图模型也可能必须计算单元格的大小/高度)基于图像大小,在单元实际配置该图像之前)。
使用这种方法是可行的,但你会再次违反一些原则,这都是你的选择。有些人说在视图模型中引用 UIKit/UIImage 是可以的(https: //lickability.com/blog/our-view-on-view-models/,不确定他们的论点是否应该被认为是有效的......)。
否则,您将不得不使用某种辅助类/对象来传递图像 url 并执行网络部分并跟踪已下载的内容、应取消的请求等。

请记住,您不必遵循严格来说是 MVC 或 MVVM。无论如何,某些对象必须下载这些图像,并且该对象理想地不应该是视图、视图模型或数据模型中的任何一个。根据需要将其命名为 ImageLoadingCoordinator、ImageRepository 等,并确保它异步下载并具有回调,并提供取消请求和缓存图像的可能性。一般来说,尽量不要赋予任何对象太多的责任,或者将网络代码与视图代码混合在一起。

First of all, you say you use MVVM but I see some issues in implementation: you are passing a Model (Article instance) to the cell, but it should be a ViewModel (ArticleViewModel instance, but you didn't create that struct) since views should not have a direct reference to models in this architecture.

You also seem to add async/networking code inside the custom cell, which also violates the architecture and separation of concerns (even in MVC, that code should not be there, since Network Requests are part of the Service Layer and not the UI layer). The cell/view should know how to configure itself: 1. when image is available and 2. when image is not available... (you call reload on the cell from VC whenever the image becomes available, for instance)

Regarding your question, a better place to add the image downloading code would be cellForRowAt method. ViewModels should not be tied to UIKit generally speaking, so having a URL or String property for the image is fine.
To make the UI display smoothly on scrolling, you probably want to cancel the request: 1. when the image is prepared for reuse aka thrown in the reuse pool or 2. when it disappears from the visible area of the screen.

Here is a good article that uses approach #1: https://www.donnywals.com/efficiently-loading-images-in-table-views-and-collection-views/

Note that it caches the images in memory, but you may also want to cache them on disk.

View Controller is generally also considered to be more of a View, so technically we are still mixing Networking with UI. Perhaps it's better to use repository pattern either directly as a dependency of the View Controller when using MVC or a dependency of VC's View Model. View Model should not be aware if the Repository uses web requests to get the images or has them cached in memory or on disk, so use a protocol and Dependency Injection for that (also View Model may have to calculate the size/height of the cell based on the image size, before the cell is actually configured with that image).
Using this approach would work, but you will again violating some principles, and it's all your choice. Some people say it's ok to have UIKit/UIImage references inside view models (https://lickability.com/blog/our-view-on-view-models/, not sure if their argument should be considered valid though...).
Otherwise, you'll have to use some kind of auxiliary class/object to pass the image urls and do the networking part and keep track of what was already downloaded, what requests should be cancelled etc.

Remember, you don't have to follow MVC or MVVM strictly. Some object has to download those images anyway, and that object ideally should not be none of the view, view model or the data model. Name it as you want, ImageLoadingCoordinator, ImageRepository etc. and make sure it downloads asynchronously and has callbacks and gives the possibility to cancel requests and cache images. Generally speaking, try not to give any object to much responsibility or mix networking code with view code.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文