ios


download image in tableView(_:, cellForRowAt:)


when i segue to a viewController with tableView, the tableviewcell immediately send a request to the server using the method fetchUserAvatar(avatarName: handler: (String) -> Void). and this method returns a url that link to the image. download it and cache the image cacheImage is an object of NSCache<NSString, UIImage>. this object cacheImage was initalised in previous view controller and being assigned from pervious to this viewContoller using prepare(for segue: UIStoryboardSegue, sender: Any?). when this viewController shows up, I can't see the image in the cell. but i pop the viewController out and segue to this viewController with tableView again. the image will show. I think(I guess) because
the images weren't all fully downloaded yet. so, i can't see the images. but if i pop out the viewController and load an object of viewController and the viewController gets the images from cache. therefore, the images could be shown.
I want to know how to avoid this problem? thanks.
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 modified it, and move the download code in viewdidload and reload the tableview, the result is the same.
Does your image view have a fixed size, or are you taking advantage of the intrinsic size? Based upon your description, I'd assume the latter. And updating the cache and reloading the cell inside fetchUserAvatar completion handler should resolve that problem.
But you have two issues here:
You should really use dataTask to retrieve the image, not Data(contentsOf:) because the former is asynchronous and the latter is synchronous. And you never want to do synchronous calls on the main queue. At best, the smoothness of your scrolling will be adversely affected by this synchronous network call. At worst, you risk having the watch dog process kill your app if the network request is slowed down for any reason and you block the main thread at the wrong time.
Personally, I'd have fetchUserAvatar do this second asynchronous request asynchronously and change the closure to return the UIImage rather than the URL as a String.
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 should not use the cell inside the asynchronously called completion handler closure. The cell could have scrolled out of view and you could be updating the cell for a different row of the table. This is likely only to be problematic for really slow network connections, but it's still an issue.
Your asynchronous closure should be determining the index path of the cell and then reloading just 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'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
}
Frankly, there are other subtle issues here (e.g. if your username or avatar name contain reserved characters, your requests will fail; if you scroll quickly on a really slow connection, images for visible cells will get backlogged behind cells that are no longer visible, you risk timeouts, etc.). Rather than spending a lot of time contemplating how to fix these more subtle issues, you might consider using an established UIImageView category that performs asynchronous image requests and supports caching. Typical options include AlamofireImage, KingFisher, SDWebImage, etc.

Related Links

tableView is not updating until i tap another tab and get back - swift
Geolocation iphone simulator does not work (iOS/Titanium)
How to retrieve certain images from Parse.com
MPMoviePlayerController and play audio from other apps, such as spotify, in background
Call CellForRow after completion block
Wait saveInBackground Parse before viewDisappear
Retrieve part of a Parse Object using Cloud Code
Which format specifier to use or how to achieve the below
What's the best way to load data for a ViewController before navigating to it?
Code error in Tic-Tac-Toe game
Braintree creditcard in UITableViewCell
Xcode use of undeclared identifier
Posting image to twitter using twitterkit
How to assign a class to a variable with JavascriptCore
How to enable developer mode on iOS device withoout starting Xcode
How to navigate particular search page in App store?

Categories

HOME
elastic-beanstalk
vb.net
anylogic
backand
quickblox
pie-chart
cross-compiling
uwsgi
doorkeeper
highlight
fbx
raspberry-pi2
aerospike
appcelerator-alloy
google-finance
advantage-database-server
pcap
amazon-cloudfront
ratchet-2
meanjs
charts.js
background-process
pymssql
gridstack
presto
docker-image
pac
subscription
textangular
formatdatetime
mobile-browser
freecodecamp
oracle-aq
node.js-tape
manual
waf
jeditorpane
masspay
cabal-install
informatica-cloud
surroundscm
winobjc
cover
intrinsics
mongolab
yapdatabase
storage-duration
screen-capture
occam-pi
relaxng
sejda
delphi-10-seattle
perfview
nbug
stringr
gevent-socketio
getuikit
physx
mpj-express
prettyfaces
knockout-mvc
bessel-functions
syndication-feed
zeroclipboard
bunny
tinkerpop-frames
textselection
magic-unipaas
qt4.7
vlfeat
mod-python
nsusernotificationcenter
mysql-backup
asp.net-session
rgba
jscience
consistent-hashing
objcopy
stumbleupon
textboxlist
renderaction
unitils
indextank
webshop
xquery-update
large-teams
metaphone
corporate-policy

Resources

Mobile Apps Dev
Database Users
javascript
java
csharp
php
android
MS Developer
developer works
python
ios
c
html
jquery
RDBMS discuss
Cloud Virtualization
Database Dev&Adm
javascript
java
csharp
php
python
android
jquery
ruby
ios
html
Mobile App
Mobile App
Mobile App