Download image in tableView (_ :, cellForRowAt :)
when i go to viewController with tableView, the tableviewcell will immediately send a request to the server using the method fetchUserAvatar(avatarName: handler: (String) -> Void)
. and this method returns the url that links to the image. load it and cache the image cacheImage
is an object NSCache<NSString, UIImage>
. this object cacheImage
was initialized in the previous view manager and was assigned from the transparent viewContoller withprepare(for segue: UIStoryboardSegue, sender: Any?)
... when this viewController comes up, I can't see the image in the cell. but I log out of the viewController and go back to that viewController using the tableView. the picture will show. I think (think) because the images haven't been fully loaded yet. therefore, I do not see the image. but if i exit viewController and load the viewController object and viewController gets images from cache. so images can be shown.
I want to know how to avoid this problem? thank.
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()
}
I changed it and moved the loading code to viewdidload and reload the tableview, the result is the same.
source to share
Does your image have a fixed size, or are you using an internal size? Based on your description, I would take on the latter. Both updating the cache and reloading the cell inside the completion handler fetchUserAvatar
should fix this issue.
But you have two problems:
-
You should really use
dataTask
to fetch the image, notData(contentsOf:)
because the former is asynchronous and the latter is synchronous. And you never want to make synchronous calls on the main queue. At best, this synchronous network call will adversely affect the smoothness of your scrolling. In the worst case, you run the risk of the clockwise process killing your application if for any reason the network request slows down and you block the main thread at the wrong time.Personally, I would
fetchUserAvatar
execute this second asynchronous request asynchronously and change the closure to returnUIImage
, not the URL asString
.Perhaps something like:
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() }
-
This is a more subtle point, but you shouldn't use
cell
a completion handler inside an asynchronously called closure. The cell may have scrolled out of sight and you could update the cell for another row in the table. This can probably be problematic for really slow network connections, but it's still a problem.Your asynchronous close should determine the cell index path and then reload only that index path with
reloadRows(at:with:)
.For example:
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 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 }
To be honest, there are other subtle issues here too (for example, if your username or avatar name contains reserved characters, your requests will fail, if you scroll quickly on a very slow connection, images for visible cells will depend on cells that are not longer , you risk latency, etc.). Instead of spending a lot of time thinking about how to fix these more subtle issues, you might consider using an established category UIImageView
that makes asynchronous image requests and supports caching. Typical options include AlamofireImage, KingFisher, SDWebImage, etc.
source to share