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

Change parameter inside dispatch_async from main thread
fb ios sdk 4.5 crashes in iOS7
How to get last frame from video?
Share content on an iOS native app via messages or email with long enough trial
UIPaveViewController with page indicator overlaying content
UITableView in Storyboard hidden first cell when it back it will OK ? why?
iOS: ActionSheetStringPicker successAction: wrong selection
Swift calling delegate from didSet
iOS 8 and segues throwing unexpected exception
<TextInput keyboardType=“numeric”/> is not working in react native
UITableViewController scrollViewDidScroll: not called during cell removal
Using NSURLProtocol with NSURLSession
Uploading file to Amazon S3 with KMS key in xcode
UIlabel multiline in UITableView
unable to set custom font in NSMutableAttributedString
How to make different search field for every sections in UITableview?

Categories

HOME
music
identityserver4
module
orbeon
path
arm
android-sms
go-gorm
microsoft-dynamics
range
intl
h2o
bluej
threadpool
openui5
yii2-advanced-app
calabash-android
installer
w2ui
durandal-2.0
servlet-filters
libpcap
google-finance
sfsafariviewcontroller
ghost-blog
software-distribution
man
similarity
hawq
pvs-studio
valueinjecter
true-type-fonts
python-behave
lync
jtapi
jformattedtextfield
nav
sagepay
word-embedding
google-slides
nexus-5
php-5.5
formatdatetime
mergesort
rtems
msysgit
distributed-lock
ssms-2012
multistore
glfw
angular-directive
graph-traversal
getelementbyid
singly-linked-list
nuitka
securesocial
groups
duration
blpapi
illegalstateexception
document-ready
interpreted-language
session-replication
cbc-mode
maven-cargo
access-log
sql-server-ce-3.5
newsql
vraptor
twitter-follow
information-schema
sqlhelper
android-menu
syndication-feed
email-spam
data-quality
delphi-2009
libavcodec
parsekit
siblings
bochs
semantic-markup
floating-point-conversion
hla
globbing
progressdialog
arden-syntax
quilt
duplicate-data
datatemplate
xmltextwriter
mkreversegeocoder
directoryentry
jdic
spquery
non-relational-database
resharper-4.5
shareware

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