【问题标题】:download image in tableView(_:, cellForRowAt:)在 tableView(_:, cellForRowAt:) 中下载图像
【发布时间】:2017-05-15 16:21:57
【问题描述】:

当我使用 tableView 转到 viewController 时,tableviewcell 立即使用方法fetchUserAvatar(avatarName: handler: (String) -> Void) 向服务器发送请求。这个方法返回一个链接到图像的 url。下载并缓存图像cacheImageNSCache<NSString, UIImage> 的对象。这个对象cacheImage 是在之前的视图控制器中初始化的,并使用prepare(for segue: UIStoryboardSegue, sender: Any?) 从可渗透的视图控制器分配给这个视图控制器。当这个 viewController 出现时,我在单元格中看不到图像。但是我将 viewController 弹出并再次使用 tableView 转到此 viewController。图像将显示。我想(我猜)因为 图像还没有完全下载。所以,我看不到图像。但是如果我弹出 viewController 并加载 viewController 的对象,并且 viewController 从缓存中获取图像。因此,可以显示图像。

我想知道如何避免这个问题?谢谢。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessageCell
    let row = indexPath.row

    cell.content.text = messages[row].content
    cell.date.text = messages[row].createdDateStrInLocal
    cell.messageOwner.text = messages[row].user

    if let avatar = cacheImage.object(forKey: messages[row].user as NSString){
        cell.profileImageView.image = avatar
    } else {
        fetchUserAvatar(avatarName: messages[row].user, handler: { [unowned self] urlStr in
            if let url = URL(string: urlStr), let data = try? Data(contentsOf: url), let avatar = UIImage(data: data){
                self.cacheImage.setObject(avatar, forKey: self.messages[row].user as NSString)

                cell.profileImageView.image = avatar
            }
        })
    }
    return cell
}

fileprivate func fetchUserAvatar(avatarName: String, handler: @escaping (String) -> Void){
    guard !avatarName.isEmpty, let user = self.user, !user.isEmpty else { return }
    let url = URL(string: self.url + "/userAvatarURL")
    var request = URLRequest(url: url!)
    let body = "username=" + user + "&avatarName=" + avatarName
    request.httpMethod = "POST"
    request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
    request.httpBody =  body.data(using: .utf8)
    defaultSession.dataTask(with: request as URLRequest){ data, response, error in
        DispatchQueue.main.async {
            if let httpResponse = response as? HTTPURLResponse {
                if 200...299 ~= httpResponse.statusCode {
                    print("statusCode: \(httpResponse.statusCode)")
                    if let urlStr = String(data: data!, encoding: String.Encoding.utf8), urlStr != "NULL" {
                        handler(urlStr)
                    }
                } else {
                    print("statusCode: \(httpResponse.statusCode)")
                    if let unwrappedData = String(data: data!, encoding: String.Encoding.utf8) {
                        print("POST: \(unwrappedData)")
                        self.warning(title: "Fail", message: unwrappedData, buttonTitle: "OK", style: .default)
                    } else {
                        self.warning(title: "Fail", message: "unknown error.", buttonTitle: "OK", style: .default)
                    }
                }
            } else if let error = error {
                print("Error: \(error)")
            }
        }
        }.resume()
}

我修改了一下,把下载代码移到viewdidload里面重新加载tableview,结果还是一样。

【问题讨论】:

  • fetchUserAvatar(avatarName: handler: (String) -> Void) 是主线程中的handler: 闭包吗?
  • 我认为你需要在获取头像后重新加载单元格。我建议您查看 SDWebImage。
  • 我添加了更多细节。
  • 但是我应该在哪里添加重新加载?

标签: ios swift uitableview


【解决方案1】:

您的图像视图是否具有固定大小,或者您是否在利用固有大小?根据你的描述,我假设是后者。更新缓存并重新加载fetchUserAvatar 完成处理程序中的单元格应该可以解决该问题。

但是这里有两个问题:

  1. 您真的应该使用dataTask 来检索图像,而不是Data(contentsOf:),因为前者是异步的,而后者是同步的。而且您永远不想在主队列上进行同步调用。充其量,您的滚动流畅度会受到这种同步网络调用的不利影响。在最坏的情况下,如果网络请求因任何原因变慢并且您在错误的时间阻塞了主线程,您就有可能让看门狗进程杀死您的应用程序。

    就个人而言,我会让fetchUserAvatar 异步执行第二个异步请求,并更改闭包以返回UIImage 而不是String 的URL。

    可能是这样的:

    fileprivate func fetchUserAvatar(avatarName: String, handler: @escaping (UIImage?) -> Void){
        guard !avatarName.isEmpty, let user = self.user, !user.isEmpty else {
            handler(nil)
            return
        }
    
        let url = URL(string: self.url + "/userAvatarURL")!
        var request = URLRequest(url: url)
        let body = "username=" + user + "&avatarName=" + avatarName
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
        request.httpBody =  body.data(using: .utf8)
    
        defaultSession.dataTask(with: request) { data, response, error in
            guard let data = data, error == nil, let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
                print("Error: \(error?.localizedDescription ?? "Unknown error")")
                DispatchQueue.main.async { handler(nil) }
                return
            }
    
            guard let string = String(data: data, encoding: .utf8), let imageURL = URL(string: string) else {
                DispatchQueue.main.async { handler(nil) }
                return
            }
    
            defaultSession.dataTask(with: imageURL) { (data, response, error) in
                guard let data = data, error == nil else {
                    DispatchQueue.main.async { handler(nil) }
                    return
                }
    
                let image = UIImage(data: data)
                DispatchQueue.main.async { handler(image) }
            }.resume()
        }.resume()
    }
    
  2. 这是一个更微妙的点,但您不应该在异步调用的完成处理程序闭包中使用cell。单元格可能已滚动到视图之外,您可能正在为表格的不同行更新单元格。这可能只对非常慢的网络连接有问题,但它仍然是一个问题。

    您的异步闭包应该确定单元格的索引路径,然后使用 reloadRows(at:with:) 重新加载该索引路径。

    例如:

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessageCell
        let row = indexPath.row
    
        cell.content.text = messages[row].content
        cell.date.text = messages[row].createdDateStrInLocal
        cell.messageOwner.text = messages[row].user
    
        if let avatar = cacheImage.object(forKey: messages[row].user as NSString){
            cell.profileImageView.image = avatar
        } else {
            cell.profileImageView.image = nil   // make sure to reset this first, in case cell is reused
            fetchUserAvatar(avatarName: messages[row].user) { [unowned self] avatar in
                guard let avatar = avatar else { return }
                self.cacheImage.setObject(avatar, forKey: self.messages[row].user as NSString)
    
                // note, if it's possible rows could have been inserted by the time this async request is done,
                // you really should recalculate what the indexPath for this particular message. Below, I'm just
                // using the previous indexPath, which is only valid if you _never_ insert rows.
    
                tableView.reloadRows(at: [indexPath], with: .automatic)
            }
        }
    
        return cell
    }
    

坦率地说,这里还有其他一些微妙的问题(例如,如果您的用户名或头像名称包含保留字符,您的请求将失败;如果您在非常慢的连接上快速滚动,可见单元格的图像将积压在没有的单元格后面更长的可见性,您可能会面临超时等)。与其花大量时间考虑如何解决这些更微妙的问题,不如考虑使用已建立的UIImageView 类别,该类别执行异步图像请求并支持缓存。典型选项包括 AlamofireImage、KingFisher、SDWebImage 等。

【讨论】:

  • 对不起,第一点我不太明白。正如你所说的在fetchUserAvatar 中获取图像,所以方法中有两个请求?
  • @CarlHung - 是的,有两个请求,一个用于获取 URL,一个用于获取图像。理想情况下,如果您可以将 Web 服务更改为仅将实际图像作为原始请求的一部分返回,那将更有效率。但是,由于您现在已经设计了 Web 服务,一个对 URL 的请求随后您必须在另一个请求中检索,那么,是的,您应该让 fetchUserAvatar 执行这两个请求。请参阅上面修订答案中的示例。
  • 我重写了方法并使用 ViewDidLoad 中的这种新方法下载图像。这是相同的。无法修复它。我需要弹出 VC 并再次对其进行 segue。那我去看看。
  • viewDidLoad?通常我们建议延迟获取图像(cellForRowAt)。如果没有看到你做了什么,我无法评论你在viewDidLoad 中的尝试。 viewDidLoad获取相关图片后如何触发相关单元格的重新加载?
  • 哦。我误解了这件事。我认为“积压”意味着更好地在其他地方获取它。我将代码放回 tableView(_:, cellForRowAt:) 它可以工作。虽然我遇到了布局问题。这是另一回事。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-07-02
  • 2017-11-01
  • 2018-05-28
  • 1970-01-01
  • 2019-12-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多