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

Align an image horizontally and vertically in swift
Pass data between View controllers without using prepareforsegue
uiWebView displays a blank white page for a certain site
UITableViewCell accessory is not updating with searchResults
Custom Callout - ViewDidLoad - Swift
Xcode autolayout - dynamic constraints
How to solve the error “:37: parser error : EntityRef: expecting ';'” in Xcode, Objective-C
iOS new view not appearing and/or old view not disappering
How to place textfield position using tap gesture
UICollectionView must be initialized with a non-nil layout parameter. error in swift with UICollectionView
How to rotate custom marker image on google map objective c
Determine “nil” value in JSON
How to Speed up the audio Playing in MpMoviePlayerController
share extension in ios 8 with objective c
Implementing pixel-perfect background images in different orientations
iAP buy button with price tier text

Categories

HOME
opencv
mongodb
ffmpeg
office-js
sharepoint-2013
ssl-certificate
configuration
haskell-stack
passwords
middleware
xquery
logback
regression
mobilefirst-adapters
push
uwsgi
pouchdb
asp.net-mvc-5.2
export-to-excel
h.264
textmatebundles
spring-restdocs
software-distribution
code-analysis
jqplot
grouping
pdfa
sql-server-2014-express
opensuse
matterjs
gecko
scom
advanced-installer
azure-app-service-envrmnt
nav
phing
titanium-alloy
weebly
dsx
firedac
essbase
easy-digital-downloads
syncfusion
mergesort
backwards-compatibility
opal
strip-tags
clientscript
vega-lite
recurrence
htc
canopen
typesetting
catalina
lcov
postmark
android-chips
rsh
android-number-picker
isapi-rewrite
intrinsics
vimeo-ios
genetic
dbfit
spl
julian
vbo
linegraph
easynetq
liferay-hook
iphone-privateapi
textselection
dirpagination
android-c2dm
execcommand
libnids
emacs-jedi
jquery-data
slimbox
timespan
proxy-classes
semantic-zoom
httpcontext.cache
mdi
boost-gil
ramaze
datatemplate
ssao
plinqo
adbwireless
chickenfoot
coords
msn-messenger
non-relational-database
code-statistics
reference-library
roguelike

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