在tableView(_:,cellForRowAt :)中下载图像

时间:2017-05-15 16:21:58

标签: ios swift uitableview

当我使用tableView转到viewController时,tableviewcell会立即使用方法fetchUserAvatar(avatarName: handler: (String) -> Void)向服务器发送请求。并且此方法返回链接到图像的URL。下载并缓存图像cacheImageNSCache<NSString, UIImage>的对象。此对象cacheImage在之前的视图控制器中被初始化,并且使用prepare(for segue: UIStoryboardSegue, sender: Any?)从此viewcontoller分配。当这个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,结果是一样的。

1 个答案:

答案 0 :(得分:2)

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

但你有两个问题:

  1. 您应该使用dataTask来检索图像,而不是Data(contentsOf:),因为前者是异步的而后者是同步的。而且你永远不想在主队列上进行同步调用。充其量,此同步网络呼叫会对滚动的平滑性产生不利影响。在最糟糕的情况下,如果因任何原因导致网络请求速度变慢并且您在错误的时间阻止主线程,那么您可能会有看门狗进程终止应用程序的风险。

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

    也许是这样的:

    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
    }
    
  3. 坦率地说,这里还有其他微妙的问题(例如,如果您的用户名或头像名称包含保留字符,您的请求将失败;如果您在非常慢的连接上快速滚动,则可见单元格的图像将在无效的单元格后面积压更长时间可见,你有超时等风险)。您可以考虑使用已建立的UIImageView类别来执行异步图像请求并支持缓存,而不是花费大量时间考虑如何解决这些更微妙的问题。典型选项包括AlamofireImage,KingFisher,SDWebImage等。